主要内容
快速排序 通常是实际排序应用中最好的选择。它在最坏情况下的时间复杂度很差,但是平均性能非常好。另外,它能够进行原址排序。
快速排序的描述
与归并排序一样,快速排序也用到了分治思想。其主过程的代码如下:
1 2 3 4 5 6 7 8 void quicksort (int A[], int p, int r) { if (p < r) { int q = partition(A, p, r); quicksort(A, p, q-1 ); quicksort(A, q+1 , r); } }
算法的关键是数组划分过程PARTITION,它实现了对子数组 的原址重排。选择 作为主元,围绕它来划分数组 。完成划分后, 的每一个元素都小于等于 ,而 也小于等于 中的每个元素。程序最后返回主元的新小标。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void Exch (int *arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } int partition (int A[], int p, int r) { int x = A[r]; int i = p - 1 ; for (int j = p; j < r; j++) { if (A[j] <= x) { ++i; Exch(A, i, j); } } Exch(A, i+1 , r); return (i + 1 ); }
PARTITION在子数组 上的复杂度为 ,其中 。
快速排序的性能
当划分产生的两个子问题分别包含了 个元素和 个元素时,快速排序的最坏情况 就发生了。假设算法的每一次递归调用中都出现了这种不平衡划分,可以得到如下的递归式:
可以得到时间复杂度为 ,也就是说,最坏情况下,快速排序的运行时间并不比插入排序好。当输入数组已经完全有序时,快速排序的时间复杂度为 ,而这种情况下插入排序的时间复杂度为 。
在可能的最平衡的划分中,PARTITION得到的两个子问题的规模都不大于 ,这便是快速排序的最好情况 。运行时间的递归式为:
上述递归式的解为 ,通过在每一层递归中都平衡划分子数组,我们得到了渐近时间上更快的算法。
实际上,只要划分是常数比例的,算法的运行时间总是 ,即使是 的划分,其时间复杂度仍然是 。当对一个随机输入的数组进行快速排序时,平均情况下,划分同时混合有「好」和「差」的划分。当好和差的划分交替出现时,快速排序的复杂度和全是好的划分时一样,仍然是 ,区别只在于隐含的常数因子要略大一些。
快速排序的随机化版本
前面的讨论基于一个前提:输入数据的所有排列都是等概率的。实际工程中并不总是这样的,所以我们可以通过在算法中引入随机性,从而使得算法对于所有的输入都能获得较好的期望性能。
可以从子数组 中随机选择一个元素作为主元,这通过将 与从 中随机选出的一个元素交换来实现。由于主元元素是随机选取的,在平均情况下,对数组的划分是比较均衡的。随机化版本的划分函数实现代码如下:
1 2 3 4 5 6 int randomized_partition (int A[], int p, int r) { int i = rand() % (r - p + 1 ) + p; Exch(A, r, i); return partition(A, p, r); }
快速排序分析
本节用严格的数学推导计算了快速排序算法的运行时间。在输入元素互异的情况下,随机化版本的期望运行时间为 。
习题解答
练习题
7.1-2 解答:返回的 值等于 。可以加一个条件判断,如果 等于 ,则返回 。
7.1-3
解答:简单的证明就是,该操作只需要遍历一趟子数组。
7.1-4
解答:将PARTITION第四行小于等于号改成大于等于号即可。
7-2-2
解答:当所有元素都相同时,PARTITION返回值为 ,会造成不平衡的划分,所以QUICKSORT的时间复杂度为 。
7-2-3
解答:当所有元素不同,而且按照降序排列时,PARTITION会返回 ,造成不平衡的划分,所以时间复杂度为 。
7-2-4
解答:严格的证明太辛苦了,算了。因为它是几乎有序的输入序列,非递归的插入排序算法的性能会优于递归的快速排序算法。
7-2-5 解答:当 时, 。如果某个分支正好每次都碰到较小的划分,则叶节点的深度最小,根据等式 求得深度 ;如果某个分支正好每次都碰到较大的划分,则叶节点的深度最大,类似地有 ,可以求得此时的深度为 。
7-3-1
解答:因为有随机性,统计意义上的平均时间才有意义。
7-3-2
解答:最坏情况下随机数生成器RANDOM被调用了 次;最好情况下随机数生成器RANDOM被调用了 次。
思考题
7-1 (Hoare划分的正确性)
根据题干中PARTITION过程的伪代码,我给出如下的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int hoare_partition (int A[], int p, int r) { int x = A[p]; int i = p - 1 ; int j = r + 1 ; for (;;) { do { --j; } while (A[j] > x); do { ++i; } while (A[i] < x); if (i < j) { Exch(A, i, j); } else { return j; } } }
习题解答如下: a)略。 b)因为两端的下表 和 分别递增和递减向中间靠拢,而且一旦有 ,函数即返回,所以不会访问到数组外的元素。 c)因为只有 函数才会返回,所以返回值 不可能等于 ,有可能等于 。 d)当 且 时,会交换两个元素的位置,所以满足题目中的条件。
e)要将主程序稍微修改一下:
1 2 3 4 5 6 7 8 void quicksort (int A[], int p, int r) { if (p < r) { int q = hoare_partition(A, p, r); quicksort(A, p, q); quicksort(A, q+1 , r); } }