快速排序详解

描述

快速排序(Quicksort) 由 C. A .R. Hoare 于1962年提出。

它采用分治策略:

  • 少于2个元素的数组不用管。
  • 分解 将数组分割为两个子数组,第一个子数组的所有元素不大于第二个子数组的所有元素。
  • 解决 递归地排序两个子数组。
  • 合并 啥也不用做,数组已经被原址排好了。

根据以上描述,容易写出快速排序的伪代码:

QUICKSORT(A, p, q)
/* Sort A[p..q] */
    if q > p
        Partition A into 2 subarrays: A[p..r], A[r+1..q]
        QUICKSORT(A, p, r)
        QUICKSORT(A, r+1, q)

实现

自然,快速排序的效率主要由分解步骤决定。通常,我们从数组中选取一个主元(pivot),不大于主元的元素划到主元左边,不小于主元的元素划到主元右边。

以 A = {13, 19, 7, 41, 2, 37, 5, 11} 为例,QUICKSORT(A, 1, 8) 的执行过程如下(主元是x,这里选取第1个元素作为主元,主元划分到左边,和主元相等的元素划分到右边;数组索引从1开始):

12345678
x=1319741237511
x=7251113x=194137
x=257x=111319x=4137
2x=571113193741
2571113193741

接下来,我们设计划分算法。

一个简单的想法是这样:开一个数组,从左往右复制划分到主元左边的元素,从右往左复制划分到主元右边的元素,主元夹在两者之间,用这个新数组覆盖原数组。开一个新数组是必须的吗?如果不是,快速排序将获得相对于归并排序的优势——原址。所为原址排序,即任何时刻最多只有常数个数据在数组之外。联想冒泡排序、插入排序,它们通过比较和交换实现原址排序。能否把比较和交换运用到我们的划分算法里呢?

单向划分

PARTITION1

经过思考,我们(实际上最初由Nico Lomuto)设计了以下算法:

PARTITION1(A, p, q)
    x = A[p] // pivot
    j = p
    for i = p+1 to q
        if A[i] < x
            j = j+1
            exchange A[i] with A[j]
    exchange A[p] with A[j]
    return j

for 循环的不变式:A[p+1..j] < x ≤ A[j+1..i-1]。初始情况,A[p+1..j]、A[j+1..i-1]是空数组。设A[p+1..j]里的元素小于x,A[j+1..i-1]里的元素大于等于x;若A[i]小于x,交换前,A[j+1]大于等于x,因此交换后A[p+1..j+1]均小于x,A[j+2..i]均大于等于x,由于赋值 j = j+1,循环不变式依然成立。结束后,i = q+1,因此A[p+1..j]里的元素小于x,A[j+1..q]里的元素大于等于x。再交换主元A[p]和A[j],形成A[p..j-1]里的元素小于x,A[j]等于x,A[j+1..q]里的元素大于等于x。

换言之,前方发现小于x的元素,就和后方已知不小于x的元素换个位置,并且让它接过标识两类元素分界处的旗子。最后,x和举旗子的那位换位置,并接过旗子,于是,它身后的所有元素小于x,面前的所有元素大于等于x。

PARTITION2

通过改进循环不变式,循环外的交换是可以省去的:

PARTITION2(A, p, q)
    x = A[p] // pivot
    j = p
    for i = p+1 to q
        if A[i] < x
            A[j] = A[i]
            A[i] = A[j+1]
            A[++j] = x
    return j

这是我的方案,不变式为 A[p..j-1] < A[j] ≤ A[j+1]。

PARTITION3

Bob Sedgewick发现,可以将Lomuto的划分方案修改为从右向左进行,并且使用A[p]作为哨兵:

PARTITION3(A, p, q)
    x = A[p]
    j = i = q+1
    do
        repeat
            i = i-1
        until A[i] >= x
        exchange A[--j] with A[i]
    while i > p
    return j

或等价地写为:

PARTITION3(A, p, q)
    x = A[p]
    j = q+1
    for i = q downto p
        if A[i] >= x
            exchange A[i] with A[--j]
    return j

第一种写法去除了不必要的i和q间的比较。

不变式:A[i..j-1] < x ≤ A[j..q]。终止时,i = p,A[j] = x,因此A[p..j-1] < A[j] ≤ A[j+1..q]。

PARTITION[123]的时间复杂度均为 Θ(n) ,所有优化仅限常数因子。

非随机算法

相应地,完整的QUICKSORT1如下:

QUICKSORT1(A, p, q)
    if p < q
        r = PARTITION[123](A, p, q)
        QUICKSORT1(A, p, r-1)
        QUICKSORT1(A, r+1, q)

这里有一个小改动。因为A[p..r-1]中元素小于A[r],A[r]小于等于A[r+1..q]中元素,所以A[r]的位置已确定,在接下来的递归步骤中可以去掉。为了正确性这个改动是必须的,否则当A[p]是数组中最大元素时算法不会终止。

设QUICKSORT1的运行时间为 T(n) ,则 T(n)=T(m)+T(nm1)+Θ(n),0mn1 。当m=0或(n-1)时, T(n)=Θ(n2) ,这是QUICKSORT1的最坏情况运行时间。

随机算法

两个子数组被划分越均匀,快速排序的时间效率越高。在PARTITION1中,选择第1个元素作为主元,因此当数组A已有序时,QUICKSORT1达到最坏运行时间。一个改进方案是随机选取一个元素与A[p]交换:

QUICKSORT2(A, p, q)
    if p < q
        i = randint(p, q) // a random number in [p, q]
        exchange A[i] with A[p]
        r = PARTITION[123](A, p, q)
        QUICKSORT1(A, p, r-1)
        QUICKSORT1(A, r+1, q)

这样我们得到一个随机算法, E[T(n)]=O(nlgn)

双向划分

PARTITION4

考虑了输入是有序数组,再考虑另一种极端情形——所有元素都相等。由Hoare设计的双向划分算法解决了这一问题。

PARTITION4(A, p, q)
    x = A[p]
    i = p-1
    j = q+1
    loop
        repeat
            j = j-1
        until A[j] <= x
        repeat
            i = i+1
        until A[i] >= x
        if i < j
            exchange A[i] with A[j]
        else
            return j

不变式(严格来讲最后一刻它是不满足的):A[p..i] ≤ x ≤ A[j..q]。终止时p ≤ j < q。

设L为左边的有序区,R为右边的有序区。在repeat…until阶段,索引i、j不会超出范围。进入外部循环前,L、R为空。5~7行,A[p]扮演着哨兵的角色;6~8行运行一遍即停止;9~12行,若j>p,则经过交换L、R一定都是非空的,否则返回j,有A[p] ≤ x ≤ A[p+1..q]。此后,L、R起到了哨兵的作用,因此只需要在第9行检查i、j是否交叉。

当A[i]=A[j]=x时,交换照样进行,使j处于靠近数组中点的位置。所有元素都相等时,快速排序不会退化到 Θ(n2)

使用双向划分

i、j交叉前一刻,所有元素已经非L即R,此后,j等于L的右边界,i等于R的左边界。对应的随机化QUICKSORT过程如下:

QUICKSORT3(A, p, q)
    if p < q
        exchange A[p] with A[randint(p, q)]
        r = PARTITION4(A, p, q)
        QUICKSORT4(A, p, r)
        QUICKSORT4(A, r+1, q)

由于p ≤ r < q,算法能停机。

稳定性

快速排序是稳定的吗?稳定的意思是:相等元素的先后次序保持不变。

对于PARTITION1,给出下列实例:
1 2(1) 2(2) 0 → 1 0 2(2) 2(1) → 0 1 2(2) 2(1)

PARTITION2:
1 2(1) 2(2) 0 → 0 1 2(2) 2(1)

PARTITION3:
1 0(1) 0(2) → 0(2) 0(1) 1

PARTITION4:
0(1) 0(2) → 0(2) 0(1)

使用PARTITION[1234]的快速排序是不稳定的。

其它优化

  • 对小数组(比如元素少于等于50个)采用插入排序。
  • 三数取中。
  • 将数组分为三个子数组,其元素和主元的大小关系分别是小于、等于、大于。

受篇幅和能力限制,暂时不作分析。

参考资料

  1. 《算法导论(第三版)》第7章 快速排序
  2. 《编程珠玑(第2版)》第11章 排序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值