算法导论第 7 章:快速排序

主要内容

快速排序通常是实际排序应用中最好的选择。它在最坏情况下的时间复杂度很差,但是平均性能非常好。另外,它能够进行原址排序。

快速排序的描述

与归并排序一样,快速排序也用到了分治思想。其主过程的代码如下:

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; // 生成从p到r的随机数
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); // 调用hoare_partition
quicksort(A, p, q); // 排序小于主元的部分,此行有修改
quicksort(A, q+1, r); // 排序大于等于主元的部分
}
}