排序算法——快速排序详解


参考《算法导论(第三版)》第七章。

快速排序是一种最坏情况时间复杂度为 O ( n 2 ) O(n^2) O(n2) 的排序算法。虽然最坏情况时间复杂度很差,但是快速排序通常是实际排序应用中最好的选择,因为它的平均性能非常好:它的期望时间复杂度为 O ( n   l o g   n ) O(n\ log\ n) O(n log n),而且 O ( n   l o g   n ) O(n\ log\ n) O(n log n) 中隐含的常数因子非常小。
快速排序是一种原址(原地、就地)排序算法,其真正的空间消耗是递归调用时的空间消耗,最优的情况下的空间消耗为 O ( l o g   n ) O(log\ n) O(log n),最坏的情况下的空间消耗为 O ( n ) O(n) O(n)

快速排序采用了分治法的思想,分治法的基本思想
(1)分解(划分):将原问题分解为若干个与原问题相似的子问题。
(2)求解:递归地求解子问题。若子问题的规模足够小,则直接求解。
(3)合并:将每个子问题的解组合成原问题的解。

快速排序的描述

下面是对一个子数组 A [ p . . r ] A[p..r] A[p..r] 进行快速排序的三个分治过程:

分解:数组 A [ p . . r ] A[p..r] A[p..r] 被划分为两个(可能为空)子数组 A [ p . . q − 1 ] A[p..q-1] A[p..q1] A [ q + 1.. r ] A[q+1..r] A[q+1..r],使得 A [ p . . q − 1 ] A[p..q-1] A[p..q1] 中的每一个元素都小于等于 A [ q ] A[q] A[q] A [ q + 1.. r ] A[q+1..r] A[q+1..r] 中的每个元素都大于 A [ q ] A[q] A[q]

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

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

QuickSort(A, p, r) {
    if p < r {
        q = Partition(A, p, r);
        QuickSort(A, p, q-1);
        QuickSort(A, q+1, r);
    }
}

数组的划分方法(一)

非递减序

Q u i c k S o r t QuickSort QuickSort 算法的关键部分是 P a r t i t i o n Partition Partition 过程,它实现了对子数组 A [ p . . r ] A[p..r] A[p..r] 的原址重排。下面给出《算法导论》中的一种实现方法:

  • 总是选择 x = A [ r ] x = A[r] x=A[r] 作为基准元素(也叫主元,pivot element);

    如果基准元素选的不是数组的最后一个元素,将其与最后一个元素互换即可,这也是随机化快速排序算法的基本思想。

  • 初始化 i = p − 1 i=p-1 i=p1,对 j = p j=p j=p j = r − 1 j=r-1 j=r1 进行循环:

    在循环体的每一轮迭代开始时,对任意数组下标 k k k,有:
    (1)若 p ≤ k ≤ i p \leq k \leq i pki,则 A [ k ] ≤ x A[k] \leq x A[k]x,围绕该元素来划分子数组。
    (2)若 i + 1 ≤ k ≤ j − 1 i+1 \leq k \leq j-1 i+1kj1,则 A [ k ] > x A[k] > x A[k]>x。(因为循环开始时, j j j 处的元素还未处理,因此不知道 A [ j ] A[j] A[j] 与基准元素的关系。)
    (3)若 k = r k = r k=r,则 A [ k ] = x A[k] = x A[k]=x

  • 最后通过将基准元素 x = A [ r ] x=A[r] x=A[r] 与最左的大于 x x x 的元素 A [ i + 1 ] A[i+1] A[i+1] 进行交换,就可以将主元素移到它在数组中的正确位置上,并返回主元的新下标 q = i + 1 q=i+1 q=i+1

至此为止,若存在的话, A [ p . . . q − 1 ] A[p...q-1] A[p...q1] 中的元素都小于等于 A [ q ] A[q] A[q] A [ q + 1.. r ] A[q+1..r] A[q+1..r] 中的元素都大于 A [ q ] A[q] A[q]

P a r t i t i o n Partition Partition 在子数组 A [ p . . r ] A[p..r] A[p..r] 上的时间复杂度为 O ( n ) O(n) O(n),其中 n = r − p + 1 n=r-p+1 n=rp+1

最后注意一点,基准元素不会被包含在后续的 Q u i c k S o r t QuickSort QuickSort P a r t i t i o n Partition Partition 的递归调用中。

Partition(A, p, r) {
    x = A[x]
    i = p-1;
    for j = p to r-1
        if A[j] <= x
            i = i+1;
            exchange A[i] with A[j];
    exchange A[i+1] with A[r];
    return i+1;
}

在这里插入图片描述

在上面的例子中显示了 P a r t i t i o n Partition Partition 算法如何在包含了 8 个元素的数组 A [ p . . r ] A[p..r] A[p..r] 上进行操作的过程。
其中数组项 A [ r ] A[r] A[r] 是基准元素 x x x浅阴影部分的数组元素都在划分的第一部分,其值都小于等于 x x x;深阴影部分的元素都在划分地第二部分,其值都大于 x x x;白色的元素代表还未分入这两个部分中的任何一个,最后的白色元素就是基准元素 x x x
(a)初始的数组和变量设置,数组元素均未放入前两个部分中的任何一个。
(b)2 与它自身进行交换,并被放入了元素值较小的那个部分。
(c ~ d)8 和 7 被添加到元素值较大的那个部分中。
(e)1 和 8 进行交换,数值较小的部分规模增加。
(f)3 和 7 进行交换,数值较小的部分规模增加。
(g ~ h)5 和 6 被包含进较大部分,循环结束。
(i)最后基准元素被交换,这样基准元素就位于这两个部分之间,返回当前基准元素的下标值。

非递增序

Q u i c k S o r t QuickSort QuickSort 算法不需要变化,只需要将 P a r t i t i n Partitin Partitin 算法中的 A[j] <= x 改为 A[j] >= x,就可以实现非递增排序,每一次对子数组 A [ p . . r ] A[p..r] A[p..r] 划分完成后, A [ p . . q − 1 ] A[p..q-1] A[p..q1] 中的元素都大于等于 A [ q ] A[q] A[q] A [ p + 1.. r ] A[p+1..r] A[p+1..r] 中的元素都小于 A [ q ] A[q] A[q]

从这里也可以看出来 Q u i c k S o r t QuickSort QuickSort 算法的关键就在于 P a r t i t i o n Partition Partition 过程的实现。

数组的划分方法(二)

同样是通过基准元素 x x x 将子数组 A [ p . . r ] A[p..r] A[p..r] 划分为两个(可能为空)子数组 A [ p . . q − 1 ] A[p..q-1] A[p..q1] A [ q + 1.. r ] A[q+1..r] A[q+1..r],使得 A [ p . . q − 1 ] A[p..q-1] A[p..q1] 中的每一个元素都小于等于 A [ q ] A[q] A[q] A [ q + 1.. r ] A[q+1..r] A[q+1..r] 中的每个元素都大于 A [ q ] A[q] A[q]

(1)以数组的第一个元素作为基准元素 x x x如果基准元素选的不是数组的第一个元素,将其与第一个元素互换即可。设置指针 i = p , j = r i=p,j=r i=p,j=r
(2)从右向左找小于等于 x x x 的元素,将其放在 A [ i ] A[i] A[i] 的位置上。
(3)从左向右找大于 x x x 的元素,将其放在 A [ j ] A[j] A[j] 的位置上。
(4)不断重复 2,3 步骤,直到指针 i i i j j j 重合,这样所有的元素都被扫描了一遍,我们将基准元素赋值给重合位置,并返回重合位置。

Partition(A, p, r) {
    i = p, j = r;
    x = A[i];           // 以最左侧元素为基准元素

    while i < j 
        while(i < j && A[j] > x) 
            j = j-1;
        if i < j
            A[i++] = A[j];

        while(i < j && A[i] <= x) 
            i = i+1;
        if i < j
            A[j--] = A[i];

    A[i] = x;
    return i;
}

性能分析

快速排序的运行时间依赖于划分是否平衡,而平衡与否又依赖于用于划分的元素(基准元素)。

最坏情况划分

若算法的每一次递归调用过程中,划分都是最大程度不平衡的,即划分产生的两个子问题分别包含了 n − 1 n-1 n1 0 0 0 个元素(因为基准元素不会被包含在后续的递归调用过程中),快速排序的最坏情况发生了。

此时算法运行时间的递归式可以表示为:

T ( n ) = T ( n − 1 ) + O ( n ) T(n) = T(n-1)+O(n) T(n)=T(n1)+O(n)

最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

最好情况划分

在可能的最平衡的划分中,划分得到的两个子问题的规模都不大于 n / 2 n/2 n/2,这是因为其中一个子问题的规模为 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor n/2,而另一个子问题的规模为 ⌈ n / 2 ⌉ − 1 \lceil n/2\rceil -1 n/21
在这种情况下,快速排序的性能非常好,此时,算法运行时间的递归式为:

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

最坏情况下的时间复杂度为 O ( n   l o g   n ) O(n\ log\ n) O(n log n)

平衡的划分

在这里插入图片描述

上图表示每次划分都是 9 : 1 9:1 9:1 时快速排序的递归树,快速排序的总代价为 O ( n   l o g   n ) O(n\ log\ n) O(n log n)
实际上,即使每次划分都是 99 : 1 99:1 99:1 的划分,其时间复杂度仍然是 O ( n   l o g   n ) O(n\ log\ n) O(n log n)事实上,任何一种常数比例的划分都会产生深度为 O ( l o g   n ) O(log\ n) O(log n) 的递归树,每一层的时间代价都是 O ( n ) O(n) O(n),算法的运行时间总是 O ( n   l o g   n ) O(n\ log\ n) O(n log n)

对于平均情况的直观观察

当对一个随机输入的数组运行快速排序时,想要像前面所假设的那样在每一层上都有同样的划分是不太可能的,我们预期某些划分会比较平衡,而另一些会很不平衡。
在平均情况下,我们假设好和差的划分是随机分布的。基于直觉,假设好和差的划分交替出现正在树的各层上,并且好的划分是最好情况划分,而差的划分是最坏情况划分。
在这种情况下,当好的划分和差的划分交替出现时,快速排序的时间复杂度与全是好的划分时一样,仍然是 O ( n   log ⁡   n ) O(n\ \log\ n) O(n log n),但 O O O 符号中隐含的常熟因子要略大一些。

快速排序的随机化版本

正如前面所说“快速排序的运行时间依赖于划分是否平衡,而平衡与否又依赖于用于划分的元素(基准元素)”,有时我们可以通过在算法中引入随机性,从而使得算法对于所有的输入都能获得较好的期望性能。很多人都选择随机化版本的快速排序作为大规模输入情况下的排序算法。

我们可以通过显式地对输入进行重复排列,使得算法实现随机化。当然,对于快速排序我们也可以这么做。但如果采用一种称为随机抽样(random sampling)的随机化技术,那么可以使得分析变得更加简单。

在上面介绍的两种划分方法中,我们始终采用最后一个元素或者第一个元素作为基准元素。以第一种划分方法为例,与始终采用 A [ r ] A[r] A[r] 为基准元素的方法不同,随机抽样是从数组 A [ p . . r ] A[p..r] A[p..r] 中随机选择一个元素作为基准元素。为了达到这一目的,首先 A [ r ] A[r] A[r] 与从 A [ p . . r ] A[p..r] A[p..r] 中随机选出的一个元素交换。

P a r t i t i o n Partition Partition Q u i c k S o r t QuickSort QuickSort 的代码的改动非常小。

Random-Partition(A, p, r) {
    i = Random(p, r);
    exchange A[r] with A[i];
    return Partition(A, p, r)
}

Random-QuickSort(A, p, r) {
    if p < r 
        q = Random-Partition(A, p, r);
        Random-QuickSort(A, p, q-1);
        Random-QuickSort(A, q+1, r);
}

使用 R a n d o m − Q u i c k S o r t Random-QuickSort RandomQuickSort,在输入元素互异的情况下,快速排序算法的期望运行时间为 O ( n   l o g   n ) O(n\ log\ n) O(n log n)

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值