对于包含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)。