快速排序的最坏情况时间复杂度为Θ(n^2)。虽然最坏情况时间复杂度很差,但是快速排序通常是实际排序应用中最好的选择,因为它的平均性能很好。它的期望运行时间复杂度为Θ(n lg n),而且Θ(n lg n)中蕴含的常数因子非常小,而且它还是原址排序的。
快速排序是一种排序算法,对包含n个数的输人数组,最坏情况运行时间为Θ(n^2) 。虽然这个最坏情况运行时间比较差,但快速排序通常是用于排序的最佳的实用选择,这是因为其平均性能相当好:期望的运行时间为Θ(nlgn), 且Θ(nlgn)记号中隐含的常数因子很小。另外, 它还能够进行就地排序,在虚存环境中也能很好地工作。
快速排序的描述:
快速排序也采用分治法进行排序,首先在数组中选择一个元素P,根据元素P将数组划分为两个子数组,在元素P左侧的子数组的所有元素都小于或等于该元素P,右侧的子数组的所有元素都大于元素P。
下面是对一个典型子数组A[p...r]排序的分治过程的三个步骤:
分解:
数组A[p..r]被划分为两个子数组A[p..q-1]和A[q+1..r]使得A[p..q-1]中的每个元素都小于等于A(q),而且,元素A(q)小于等于A[q+1..r]中的元素。下标q也在这个划分过程中进行计算。
解决:
通过递归调用快速排序,对子数组A[p..q-1]和A[q+1...r]排序。
合并:
因为两个子数组是就地排序的,将它们的合并不需要操作:整个数组A [ p.. r ] 己排序。
伪代码:
//快速排序
QUICKSORT(A,p,r)
if p < r //当数组中只剩一个元素时,退出递归,数组已经有序
q <- PARTITION(A,p,r)
QUICKSORT(A,p,q-1) //对小于等于A[q]的元素进行递归
QUICKSORT(A,q+1,r) //对大于A[q]的元素进行递归
//数组划分
PARTITION(A,p,r)
x <- A[r] //将A[r]选取为“主元”
i <- p - 1 //因为可能不存在小于等于A[r]的元素,所以i的值由p-1开始
for j <- p to r-1
if A[j] <= x
i <- i + 1 //小于等于x的元素增加一个
exchange A[i] <-> A[j]
//i+1后,i指向了一个大于x的元素,此时,j指向的是一个小于等于x的元素,交换这两个元素的位置,使其符合规则
exchange A[i+1] <-> A[r] //r前所有元素比较完后,将A[r]置于正确位置:两个子数组的交界处
return i + 1 //返回"主元"的位置
如:
习题:
7.1-2:
当数组A[p.. r] 中的元素均相同时, PARTITION返回的q值是什么?修改PARTITION,使得当数组A[p.. r] 中所有元素的值相同时, q = (p+r)/2 。
返回最后一个元素r 。
修改PARTITION:
PARTITION_1(A,p,r)
x <- A[r]
i <- p - 1
count <- 0
//加入一个计数器
for j <- p to r - 1
if A[j]<= x
if A[i] == x
count = count + 1
i <- i + 1
exchange A[i] <-> A[j]
if count == r - p + 1
return (p + r)/2
exchange A[i + 1] <-> A[r]
return i + 1
快速排序的性能:
快速排序的运行时间依赖于划分是否平衡,如果划分平衡,那么快速排序算法性能与归并排序一样,如果划分不平衡,那么快速排序的性能就接近于插入排序了。
最坏情况划分(不平衡):
最坏情况下,每次划分的两个子问题都分别包含了n-1个元素和0个元素。划分操作的时间复杂度是 Θ(n),因为对一个大小为0的数组进行递归调用后,返回了T(n)=O(1),故算法的运行时间可递归的表示为:
T(n) = T(n-1) + T(0) + Θ(n) =T(n-1) + Θ(n)
该递归式的解为:T(n) =Θ(n^2)。因此,最坏情况下,也就是数组中元素已经排好序的时候,快速排序的时间复杂度为Θ(n^2),而在同样的情况下,插入排序的时间复杂度为O(n)。
最好情况划分(最平衡):
最好的情况下,每次划分都是平均的划分为n/2个元素子数组,此时递归式为:
T(n) = 2T(n/2) +T(0) + Θ(n)
该式的解为T(n) = Θ(nlgn)。
快速排序的平均运行时间更接近于其最好情况,而非最坏情况。
事实上,任何一种常数比例的划分都会产生深度为Θ(nlgn)的递归树,其中每一层的代价都是O(n),因此,只要划分是常数比例的,算法的运行时间总是O(nlgn)。
如:
例如,假设划分过程总是产生9 : 1 的划分,乍一看这种划分很不平衡,这时,快速排序运行时间的递归式为
T(n) <= T(9n/10) + T(n/10) + cn
该树每一层的代价都是cn, 直到在深度处达到边界条件时为止,在此之下各层的代价至多为cn。递归于深度处终止。这样,快速排序的总代价为O(nlgn) 。
当数据量很小的时候,比如只有十几个元素的小型序列,快排的优势并不明显,甚至比插入排序慢。但是一旦数据多,它的优势就充分发挥出来了。
C++ STL 中的sort函数,就充分发挥了快排的优势,并且取长补短,在数据量大时采用QuickSort,分段递归排序。一旦分段后的数据量小于某个门槛,为避免QuickSort递归调用带来过大的额外负荷,就改用插入排序。如果递归层次过深,还会改用HeapSort(堆排序)。所以说,C++的“混合兵种”sort的性能肯定会比C的qsort好。
习题:
7.2-1:
利用代换法证明: 递归式的解为,如第7. 2 节开头提到的那样。
这里只证明上界,下界可类似证明。
猜测
综合上下界有T(n)=Θ(n2)。
7.2-2:
当数组A 的所有元素都具有相同值时, QUICKSORT 的运行时间是什么?
当数组A的所有元素都具有相同值时,此时出现最坏情况,每次划分会出现大小为0和n−1的子数组,此时时间复杂度为Θ(n2)。
快速排序的随机化版本:
当输入的数据是随机排列的时候,快速排序的时间复杂度是O(nlgn)。但是在实际中,输入并不总是随机的,因此需要在算法中引入随机性,可以对输入进行重新排列是算法实现随机化, 也可以进行随机抽样,随机抽样是从数组A[p…r]中随机选择一个元素作为主元。
伪代码:
Randomized-Partition(A, p, r)
i <- Random(p, r)
exchange A[r] <-> A[i]
return Partition(A, p, r)
Randomized-Quick-sort(A, p, r)
if p < r
q <- Randomized-Partition(A, p, r)
Randomized-Quick-sort(A, p, q-1)
Randomized-Quick-sort(A, q+1, r)
使用尾递归优化快速排序:
传统的递归算法在很多时候被视为洪水猛兽. 它的名声狼籍, 好像永远和低效联系在一起,尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。
QUICKSORT算法包含两个对其自身的递归调用,即调用PARTITION后,左边的子数组和右边的子数组分别被递归排序。
QUICKSORT中的第二次递归调用并不是必须的,可以用迭代控制结构来代替它,这种技术叫做“尾递归”,大多数的编译器也使用了这项技术。
下面的伪代码模拟了尾递归:
QUICKSORT'(A, p, r)
while p < r
do ▸ Partition and sort left subarray.
q <- PARTITION(A, p, r)
QUICKSORT'(A, p, q - 1)
p <- q + 1
注意第一行是while而不是if。
上面的版本在最坏的情况下,就是划分不好的时候,递归深度为O(n)。
我们还可以进一步优化,用二分思想,为了使最坏情况下栈的深度为Θ(lgn),我们必须让PARTITION后左边的子数组为原来数组的一半大小,这样递归的深度最多为Θ(lgn)。
一种处理方案:
首先求得(A, p, r)的中位数,作为PARTITION的枢轴元素,这样可以保证左右两边的元素的个数尽可能的均衡。
因为求中位数的过程MEDIAN的时间复杂度为Θ(n),因此可以保证算法的期望的时间复杂度O(nlgn)不变。
优化后的尾递归快排:
QUICKSORT (A, p, r)
while p < r
do Partition and sort the small subarray Prst
q <- PARTITION(A,p,r)
if q - p < r - q
then QUICKSORT (A, p, q - 1)
p <- q + 1
else QUICKSORT (A, q + 1,r)
r <- q - 1
除此之外,快排还可以有优化:
三数取中:
从子数组中随机选出三个元素,取其中间数作为主元,不过这只能影响快速排序时间复杂度O(nlgn)的常数因子。
非递归方法:
即模拟递归,这样可以完全消去递归的调用。
三划分快速排序:
基本思想是,在划分阶段以V=A[r]为基准,将带排序数组A[p..r]划分为左、中、右三段A[p,j],A[j+1..q-1],A[q..r],其中左段数组元素值小于V,中断数组等于V,有段数组元素大于V。其后,算法对左右两段数组递归排序。 这个方法对于有大量相同数据的数组排序效率有很大的提高,即使没有大量相同元素,也不降低原快排算法的效率。
快速排序分析:
利用RANDOMIZED-PARTITION,快速排序算法期望的运行时间当元素值不同时,为O(nlgn)。
习题:
7.4-1:
证明:在递归式:中,T(n)=Ω(n^2)。
使用代换法。
猜测 T(n) <= c*n^2,c为某个常数。
选择足够大的c,使得 c*(2*n-1)可以支配Θ(n), T(n) <= c*n^2成立。
7.4-4:
证明:RANDOMIZED-QUICKSORT 算法的期望运行时间是Ω(nlgn)。