算法导论学习笔记 第7章 快速排序

对于包含n个数的输入数组来说,快速排序是一种时间复杂度为O(n^2)的排序算法。虽然最环情况的复杂度高,但是快速排序通常是实际应用排序中最好的选择,因为快排的平均性能非常好:它的期望复杂度是O(nlgn),而且O(nlgn)中的常数因子非常小。另外,快速排序还可以实现原址排序,甚至在虚拟环境中也能很好的工作。

1 快速排序的描述

与归并排序一样,快速排序也使用了分治法的思想,下面是对一个典型的子数组A[p.. r]进行快速排序的分治过长:

分解:数组A[p.. r]被划分为两个(可能为空)子数组A[p.. q-1]和A[q+1.. r],使得A[p.. q-1]中的每一个元素都小于A[q],而A[q+1..
r]中的每个元素都大于 A[q]。q也是划分过程的一部分。

解决:通过递归调用快速排序,对子数组A[p..
q-1]和A[q+1.. r]进行排序

合并:因为子数组都是原址排序的,所以不需要合并,数组A[p..
r]已经排序。

下面是快速排序的为代码:

QUICKSORT(A, p, r)
1 if p<r
2    q = PARTITION(A, p, r)
3    QUICKSORT(A, p, q-1)
4    QUICKSORT(A, q+1, r)

为了排序一个数组A的全部元素,初始调用QUICKSORT(A, 1, A.length)。

2 数组的划分

算法的关键部分是PARTION过程,他实现了对子数组A[p.. r]的原址排序。PARTION的伪代码表示如下:

PARTION(A, p, r)
1     x = A[r]
2     i = p - 1
3     for j=p to r-1
4          if A[u]<=x
5              i = i +1
6              exchange A[i] with A[j]
7     exchange A[i+1] with A[r]
8     return i + 1       

下图表示了PARTION如何在一个包含8个元素的数组上进行操作的过程。PRATION总是选择一个x=A[r]作为主元(pivot element),并围绕它来划分数组A[p.. r] 。

PRTION在子数组A[p.. r]上的实际复杂度是O(n),其中n = r - p +1。

3 快速排素的性能

快速排序的运行时间依赖于划分是否平衡,而平衡与否又依赖于划分的元素。如果划分是平衡的,那么快速排序算法性能与归并排序一样,如果划分是不平衡的,那么快速排序的性能就接近与插入排序了,下面给出了快速排序性能的非形式化的分形:

最坏情况的划分:

当划分产生的两个子问题分别包含了n-1个元素和0个元素是,快速排序的最坏情况发生了。不妨假设算法的每一次递归调用都出现了这种不平衡的划分。划分操作的时间复杂度是O(n)。由于对一个大小为0的数组递归调用谁直接返回,因此,T(0)=O(1),于是算法运行时间的递归式可以表示为:

T(n) = T(n-1) + T(0) + O(n) = T(n-1) + O(n)

利用带入法可以直接得到递归式的解为T(n) = O(n^2)。因此,如果在算法的每一层递归上,划分都是最大程度不平衡,那么算法的时间复杂度为O(n^2)。

最好情况的划分

在可能的最平衡的划分中,PARTION得到的两个字问题的规模都不大于n/2。这是因为一个子问题的规是n/2,而另一个字问题的规模为n/2 - 1。此种情况下,快速排序的性能非常好。此时,算法的运行时间的递归式为:

T(n) = T(n/2)  + O(n)

由主定理可知,上述递归式的解为O(nlgn)。

平衡的划分

快速排序的平均运行时间更接近于其最好情况,而非最坏情况。假设的算法总是产生9:1的划分,乍一看,这种划分是很不平衡的。这时候得到的快速排序的时间复杂度的递归为:

T(n) = T(9n/10) + T(n/10) + cn

书中采用了递归树的方式求出了上述递归的解为O(nlgn)。而且指出,只要划分是常数比例的,算法的运行时间总是O(nlgn)。

时间: 2024-12-23 20:48:46

算法导论学习笔记 第7章 快速排序的相关文章

算法导论学习笔记——第7章 快速排序

快速排序 1 QUICKSORT(A,p,r) 2 if p<r 3 then q←PARTITION(A,p,r) 4 QUICKSORT(A,p,q-1) 5 QUICKSORT(A,q+1,r) 6 7 PARTITION(A,p,r) 8 x←A[r] 9 i←p-1 10 for j←p to r-1 11 do if A[j]<=x 12 then i←i+1 13 exchange A[i]↔A[j] 14 exchange A[i+1]↔A[r] 15 return i+1 随

算法导论学习笔记——第12章 二叉查找树

二叉查找树性质 设x是二叉查找树中的一个结点,如果y是x的左子树中的一个结点,则k[y]<=key[x]:如果y是右子树中的一个结点,则k[y]>=k[x] 1 //中序遍历算法,输出二叉查找树T中的全部元素 2 INORDER-TREE-WALK(x) 3 if x!=nil 4 then INORDER-TREE-WALK(left[x]) 5 print key[x] 6 INORDER-TREE-WALK(right[x]) 查找 1 //递归版本 2 TREE-SEARCH(x,k)

算法导论学习笔记——第1章

所谓算法,就是定义良好的计算过程,它取一个或一组值作为输入,并产生出一个或一组值作为输出.亦即,算法是一系列的计算过程,将输入值转换成输出值. 一些常见的算法运行时间量级比较:对数级<多项式级<指数级<阶乘级 1 lgn < n 1/2 < n < nlgn < n 2 < n 3 < 2 n < n!

算法导论学习笔记——第8章 线性时间排序

任意一种比较排序算法,在最坏情况下的运行时间下限是Ω(nlgn) 计数排序 假设n个输入元素中的每一个都是介于0到k之间的整数,k为某个整数,当k=O(n)时,计数排序的运行时间为Θ(n) 1 //输入数组A[1..n],存放排序结果数组B[1..n],临时存储区C[0..k] 2 COUNTING-SORT(A,B,k) 3 for i←0 to k 4 do C[i]←0 5 for j←1 to length[A] 6 do C[A[j]]←C[A[j]]+1 7 for i←1 to k

算法导论学习笔记——第11章 散列表

直接寻址表 1 DIRECT-ADDRESS-SEARCH(T,k) 2 return T[k] 3 4 DIRECT-ADDRESS-INSERT(T,x) 5 T[key[x]]←x 6 7 DIRECT-ADDRESS-DELETE(T,x) 8 T[key[x]]←nil

算法导论学习笔记——第6章

堆 堆数据结构是一种数组对象,可以被视为一棵完全二叉树. 对于给定的数组A,树的根为A[1],对于给定的下标为i的结点A[i],其父结点PARENT(i)=floor(i/2),左子结点LEFT(i)=2i,右子结点RIGHT(i)=2i+1 叶级结点的高度可以认为是0,每向上一层,高度加一,定义树的告诉为根结点的高度. P74

算法导论学习笔记——第4章

解递归式 1.代换法substitution 1)猜测解的形式 2)用数学归纳法找出使解真正有效的常数 2.递归树 使用递归树时,可以忽略一些“小误差”,将递归产生的结果作为猜测,用代换法进行验证. 也可以严格计算每一层递归树的代价,加总成递归式的结果. 对于有两个子问题,子问题规模为1/2的递归树(二叉树),树的高度是lgn,叶级节点的数量是n 3.主方法master method 递归式形式T(n)=aT(n/b)+f(n),其中a>=1,保证有一个及以上子问题:b>1,保证问题的规模逐步

算法导论学习笔记——第10章 基本数据结构

栈 1 Stack-EMPTY(S) 2 if top[S]=0 3 then return TRUE 4 else return FALSE 5 6 PUSH(S,x) 7 top[S]←top[S]+1 8 S[top[S]]←x 9 10 POP(S) 11 if STACK-EMPTY(S) 12 then error "underflow" 13 else top[S]←top[S]-1 14 return S[top[S]+1] 队列 1 ENQUEUE(Q,x) 2 Q[

算法导论学习笔记——第3章

一些数学问题 1.对任意两个函数f(n)和g(n),f(n)=Θ(g(n))当且仅当f(n)=O(g(n))和f(n)=Ω(g(n)) 2.实数集有一个属性不能应用在渐进符号上 三分性:对于实数a和b,下列三种情况有且仅有一种情况成立,a>b,a=b,a<b 并不是所有的函数都可以进行渐进比较 3.近似的函数增长 对数<线性<多项式<指数<阶乘