前言
快速排序是用于排序的最佳实用选择(jre中默认)。它最坏情况时间复杂度为𝑄(𝑛2)的排序算法,但它的平均性能非常好,期望时间复杂度为𝑄(𝑛𝑙𝑜𝑔𝑛),并且𝑛𝑙𝑜𝑔𝑛中隐含的常数因子非常小。
快速排序的分治过程
快速排序使用了分治的思想。下面是对一个典型的子数组𝐴[𝑝,𝑟]进行快速排序的三步分治过程:
- 分解:数组A[p,r]被划分为两个(可能为空)子数组𝐴[𝑝…𝑞−1]和𝐴[𝑞+1…𝑟],使得𝐴[𝑝…𝑞−1]中的每一个元素都小于等于𝐴[𝑞],而𝐴[𝑞]也小于等于𝐴[𝑞+1…𝑟]中的每一个元素。其中计算下标𝑞也是划分过程的一部分。
- 解决:通过递归调用快速排序,对子数组𝐴[𝑝…𝑞−1]和𝐴[𝑞+1…𝑟]进行排序
- 合并:因为子数组都是原址排序的,所以不需要排序操作:数组𝐴[𝑝…𝑟]已经有序。
QuickSort(A,p,r)
if p < r
q = Partition(A,p,r)
QuickSort(A,p,q-1)
QuickSort(A,q+1,r)
Partition(A,p,r)
x = A[r]
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
在Partition第4-7行循环体的每一轮迭代开始时,对于任意数组下标𝑘有
- 若𝑝≤𝑘≤𝑖,则𝐴[𝑘]≤𝑥。
- 若𝑖+1≤𝑘≤𝑗−1,则𝐴[𝑘]>𝑥。
- 若𝑘=𝑟,则𝐴[𝑘]=𝑥。
在子数组𝐴[𝑝…𝑟]中,Partition维护了四个区域。
- 𝐴[𝑝…𝑖]区间内所有值都小于等于𝑥,
- 𝐴[𝑖+1…𝑗−1]区间内的所有值都大于𝑥,
- 𝐴[𝑟]=𝑥。
- 𝐴[𝑗…𝑟−1]中的末处理
练习
1-1
蓝色部分代表不大于pivot,红色部分表示大于pivot
1-2
当所有的元素都相同的时候q=r,这是因为该算法结束后有 a q < a i , ( q < i ≤ r ) a_q \lt a_i, \ (q \lt i \le r) aq<ai, (q<i≤r),所以没有任何元素会在A[q]之后。将算法变成交替地将等于pivot的元素放到大小两个集合中,这样就能使得 q = ⌊ ( p + r ) / 2 ⌋ q=\lfloor (p+r)/2 \rfloor q=⌊(p+r)/2⌋
Partition(A, p, r)
x = A[r]
f = 0
i = p - 1
for j = p to r - 1
if x > A[i] or (f > 0 and x == A[i])
i = i + 1
exchange A[i] with A[j]
f = f xor 1 //异或操作
exchange A[i + 1] with A[r]
return i + 1
1-3
由于j从p变为r-1,而循环内的操作运行时间都与输入规模无关为O(1),循环共进行r-p次,所以总的时间复杂度为O(r-p)=O(n)。
1-4
只要把比pivot小的元素放到后边,把比pivot大的元素放在前边即可。
Partition(A, p, r)
x = A[r]
i = p - 1
for j = 1 to r - 1
if A[i] > x
i = i + 1
exchange A[i] with A[j]
exchange A[i + 1] with A[r]
return i + 1
快速排序的性能
快速排序的运行时间依赖于划分是否平衡,二平衡与否又依赖于划分的元素
- 最坏情况划分:当划分的两个子问题分别包含了𝑛−1个元素和0个元素时,递归式为 𝑇(𝑛)=𝑇(𝑛−1)+Θ(𝑛) , 得𝑇(𝑛)=Θ(𝑛2) 。
- 最好情况划分:在可能的最平衡划分中,Partition得到的两个子问题的规模都不大于𝑛/2。递归式为𝑇(𝑛)=2𝑇(𝑛/2)+Θ(𝑛) ,得 𝑇(𝑛)=Θ(𝑛𝑙𝑜𝑔𝑛)
- 平衡的划分:快速排序的平均运行时间更接近于其最好情况,假设划分比例为9:1,递归式为𝑇(𝑛)=𝑇(9𝑛/10)+𝑇(1𝑛/10) +Θ(𝑛) ,得 𝑇(𝑛)=Θ(𝑛𝑙𝑜𝑔𝑛)
事实上,任何一种常数比例的划分都会产生深度为Θ(𝑙𝑜𝑔𝑛)的递归树,其中每一层的时间代价都是𝑂(𝑛)。因此只要划分是常数比例的,算法的运行时间总是𝑂(𝑛𝑙𝑜𝑔𝑛)。
练习
2-1
假设T(n)=Ο(n^2),即存在 T(n)<=
c
n
2
cn^2
cn2
2-2
当数组A所有元素相同时,QUICKSORT中的划分时极为不平衡的,n-1:0的划分,T(n)=T(n-1)+Θ(n)解这个递归式T(n)=Θ(n^2)
2-3
按照降序排序时,在QUICKSORT中的划分时极为不平衡的,n-1:0的划分,所以其时间复杂度为T(n)=T(n-1)+Θ(n)解这个递归式 T(n)=T(n)=Θ(n^2)
2-4
- 插入排序在基本有序的情况下,基本无需移动任何元素来插入,所以只有外层循环了n次,所以时间复杂度为O(n)
- 快速排序在基本有序的情况下,在划分数组时,划分得非常不平衡,那么其时间复杂度是O(nlgn),而达到完全有序时,时间复杂度达到O(n^2)
2-5
- a<=1/2 => a<1-a
- 最小深度达成条件全是a这边分裂k次
- 假设数组长度为n,即 n * a^k = 1
- 最小深度k = -lgn/lga
- 最大深度k=-lgn/lg(1-a)
2-6
- 当仅仅当, 划分比在 [a,1/2]区间的时候,更平衡
- (1/2-a)/1/2 = 1-2a
快速排序的随机化版本
在讨论快速排序的平均情况时,我们的假设前提是:输入数据的所有排列都是等概率的。但是在实际中,这个假设不会总成立。
采用随机抽样(random sampling)的随机化技术。随机抽样是从子数组𝐴[𝑝…𝑟]中随机选择一个元素作为主元。为达到这一目的,首先将𝐴[𝑟]与从𝐴[𝑝…𝑟]中随机选择的一个元素交换位置。
Randomized_QuickSort(A,p,r)
if p < r
q = Randomized_Partition(A,p,r)
Randomized_Partition(A,p,q-1)
Randomized_Partition(A,q+1,r)
Randomized_QuickSort(A,p,r)
i = Random(p,r)
exchange A[r] with A[i]
return Partition(A,p,r)
我们可以保证主元元素𝑥=𝐴[𝑟]是等概率地从子数组的𝑟−𝑝+1个元素中选取的。因为主元元素是随机选取的,我们期望在平均情况下,对输入数组的划分是比较均衡的。
练习
3-1
随机化算法不能改变最坏情况下得运行时间,但是能降低最坏情况发生的概率。
3-2
- 最好情况是均匀划分,其时间复杂度 T(n)=2T(n/2)+1 =>T(n)=Θ(n)
- 最坏情况是分成不平衡的划分,其时间复杂度 T(n)=T(n-1)+T(0)+1 各式相加得=>T(n)=Θ(n)
快速排序的期望运行时间
我们首先注意到每一对元素至多比较一次。因为各个元素只与主元元素进行比较,并且在某一次的Partition调用结束之后,该元素就再也不会与其他元素进行比较了。
我们的分析要用到指示器随机变量。定义
𝑋 𝑖 𝑗 = 𝐼 { 𝑧 𝑖 与 𝑧 𝑗 进 行 比 较 } = { 1 如 果 𝑧 𝑖 与 𝑧 𝑗 进 行 比 较 发 生 0 如 果 𝑧 𝑖 与 𝑧 𝑗 进 行 比 较 不 发 生 𝑋_{𝑖𝑗}=𝐼\{𝑧_𝑖与𝑧_𝑗进行比较\}=\begin{cases} 1 &\text如果𝑧𝑖与𝑧𝑗进行比较发生 \\ 0 &\text如果𝑧𝑖与𝑧𝑗进行比较不发生 \end{cases} Xij=I{zi与zj进行比较}={10如果zi与zj进行比较发生如果zi与zj进行比较不发生
由于每一对元素至多比较一次,所以我们可以计算出算法的总比较次数:
X
=
∑
i
=
1
n
−
1
∑
j
=
i
+
1
n
X
i
j
X=\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}X_{ij}
X=i=1∑n−1j=i+1∑nXij
对上式两边取期望
E
(
X
)
=
E
(
∑
i
=
1
n
−
1
∑
j
=
i
+
1
n
X
i
j
)
=
∑
i
=
1
n
−
1
∑
j
=
i
+
1
n
P
r
{
𝑧
𝑖
与
𝑧
𝑗
进
行
比
较
}
E(X)=E(\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}X_{ij}) =\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}Pr\{𝑧_𝑖与𝑧_𝑗进行比较\}
E(X)=E(i=1∑n−1j=i+1∑nXij)=i=1∑n−1j=i+1∑nPr{zi与zj进行比较}
- 假设每一个元素是互异的。
- 一旦一个满足𝑧𝑖<𝑥<𝑧𝑗的主元𝑥被选择后,我们就知道𝑧𝑖和𝑧𝑗以后再也不可能进行比较了
- 另一种情况,如果𝑧𝑖在𝑍𝑖𝑗中的所有其他元素之前被选择为主元,那么𝑧𝑖就将与除了它自身以外的所有元素进行比较
- 如果𝑧𝑗在𝑍𝑖𝑗中的所有其他元素之前被选择为主元,那么𝑧𝑗就将与除了它自身以外的所有元素进行比较。
𝑧𝑖与𝑧𝑗会进行比较,当且仅当𝑍𝑖𝑗中被选为主元的第一个元素是𝑧𝑖或者𝑧𝑗。
于是综上两式我们有:
E
(
X
)
=
∑
i
=
1
n
−
1
∑
j
=
i
+
1
n
2
j
−
i
+
1
E(X) =\sum_{i=1}^{n-1}\sum_{j=i+1}^{n} \frac{2}{j-i+1}
E(X)=i=1∑n−1j=i+1∑nj−i+12
在求这个累加和时。可以将变量做个变换(𝑘=𝑗−𝑖),并利用有关调和级数的界,得到:
E ( X ) = ∑ i = 1 n − 1 ∑ j = i + 1 n 2 j − i + 1 = ∑ i = 1 n − 1 ∑ k = 1 n − i 2 k + 1 < ∑ i = 1 n − 1 ∑ k = 1 n 2 k = ∑ i n − 1 O ( lg n ) = O ( n lg n ) E(X) =\sum_{i=1}^{n-1}\sum_{j=i+1}^{n} \frac{2}{j-i+1}=\sum_{i=1}^{n-1}\sum_{k=1}^{n-i} \frac{2}{k+1} < \sum_{i=1}^{n-1}\sum_{k=1}^{n} \frac{2}{k} = \sum_i^{n-1}O(\lg n) = O(n \lg n) E(X)=i=1∑n−1j=i+1∑nj−i+12=i=1∑n−1k=1∑n−ik+12<i=1∑n−1k=1∑nk2=i∑n−1O(lgn)=O(nlgn)
在输入元素互异的情况下,快速排序的期望运行时间为𝑂(𝑛𝑙𝑜𝑔𝑛)。
练习
4-1
4-2
最好情况就是均分的情况下,T(n)=2T(n/2)+Θ(n) 满足主定理case2=>T(n)=Θ(nlgn)=Ω(nlgn)
4-3
求抛物线方法(过程略)
4-4
4-5
证明过程略,k值如下
4-6
最坏概率 p(x<1-a∪x>a)=p(x>a)+p(x<1-a)=1-p(a<x<1-a)=1-(2a-1)=2-2a
思考题
7-1(Hoare划分的正确性) 本章中的PARTITION算法并不是其最初版本。下面给出的是最早由C.R.Hoare所设计的划分算法:
HOARE-PARTITION(A, p, r)
x = A[p]
i = p - 1
j = r + 1
while true
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. 试说明 HOARE-PARTITION 在数组A={13,19,9,5,12,8,7,4,11,2,6,21}上的操作过程,并说明在每一次执行4-13行while循环时数组元素的值和辅助变量的值。
x=13, j = 9 and i=10
b.下标i和j可以使我们不会访问在子数组A[p…r]以外的数组A的元素
p≤i<j≤r
c.当HOARE-PARTITION结束时,它返回的值j 满足p<=j<r
略
d. 当HOARE-PARTITION结束时,A[p…r]中的每一个元素都小于或等于A[j+1…r]中的元素
略
e.利用HOARE-PARTITION,重写QUICKSORT算法
HOARE-QUICKSORT(A, p, r)
if p < r
q = HOARE-PARTITION(A, p, r)
HOARE-QUICKSORT(A, p, q)
HOARE-QUICKSORT(A, q + 1, r)
7-2 随机化快速排序的分析中,我们假设输入元素的值是互异的,在本题中,我们将看看如果这一假设不成立会出现什么情况?
a.如果所有输入元素的值都相同,那么随机化快速排序的运行时间会是多少?
如果所有输入元素的值都相同,那么每次划分都是极不平衡的。每次都是T(n)=T(0)+T(n-1)+Θ(n) T(n)=Θ(n^2)
b.PARTITION过程返回一个数组下标q,使得A[p…q-1]中的每个元素都小于等于A[q],而A[q+1…r]中的每个元素都大于A[q]。修改PARTITION代码来构造一个新的PARTITION‘(A,p,r),它排列A[p…r]的元素,返回值是两个数组下标q和t,其中p<=q<=t<=r.
- A[q…t]中的所有元素都相等。
- A[p…q-1]中的每个元素都小于A[q]。
- A[t+1…r]]中的每个元素都大于A[q]。
PARTITION'(A, p, r)
x = A[p]
low = p
high = p
for j = p + 1 to r
if A[j] < x
y = A[j]
A[j] = A[high + 1]
A[high + 1] = A[low]
A[low] = y
low = low + 1
high = high + 1
else if A[j] == x
exchange A[high + 1] with A[j]
high = high + 1
return (low, high)
c.将RANDOMIZED-QUICKSORT过程改为调用PARTITION’,并重新命名为RANDOMIZED-QUICKSORT‘。修改QUICKSORT的代码构造一个新的QUICKSORT’(A,p,r),它调用RANDOMIZED-QUICKSORT‘,并且只有分区内的元素互异时候才做递归调用。
QUICKSORT(A, p, r)
if p < r
(low, high) = RANDOMIZED-PARTITION(A, p, r)
QUICKSORT(A, p, low - 1)
QUICKSORT(A, high + 1, r)
d.在QUICKSORT‘中,应该如何改变7.4.2节中的分析方法,从而避免所有元素都是互异的这一假设?
(略)
7-3 (略)
7-4(快速排序的栈深度) 7.1节中的QUICKSORT算法包含了两个对其自身的递归调用。在调用PARTITION后,QUICKSORT分别递归调用了左边的子数组和右边的子数组。QUICKSORT中的第二个递归调用并不是必须的。我们可以用一个循环控制结构来代替它。这一技术成为尾递归,好的编译器都提供这以功能。考虑下面这个版本的快速排序,它模拟了尾递归情况。
TAIL-RECURSIVE-QUICKSORT(A, p, r)
while p < r
// Partition and sort left subarray.
q = PARTITION(A, p, r)
TAIL-RECURSIVE-QUICKSORT(A, p, q - 1)
p = q + 1
a.证明:TAIL-RECURSIVE-QUICKSORT(A,1,A.length)能正确地对数组A进行排序
略
b.请描述一种场景,使得针对一个包含n个元素数组的TAIL-RECURISIVE-QUICKSORT的栈深度是Θ(n).
自身递归调用不超过n次。每次调用过程值需要O(1)的内存空间,所以不超过n次调用就是Θ(n)空间
c.修改TAIL_RECURSIVE_QICKSORT的代码,使其最坏情况下栈深度是Θ(lgn),并且能够保持O(nlgn)的期望时间复杂度。
MODIFIED-TAIL-RECURSIVE-QUICKSORT(A, p, r)
while p < r
q = PARTITION(A, p, r)
if q < floor((p + r) / 2)
MODIFIED-TAIL-RECURSIVE-QUICKSORT(A, p, q - 1)
p = q + 1
else
MODIFIED-TAIL-RECURSIVE-QUICKSORT(A, q + 1, r)
r = q - 1
主要参考
《算法导论课后习题解析 第七章》
《算法导论第七章课后答案》
《Analysis of quicksort》
《算法导论 快速排序算法学习》
《算法导论第七章最后思考题》