快速排序是一種原地排序演算法,其最壞的已耗用時間為n2,期望的已耗用時間為nlgn,且隱含的常數因子很小。所以快速排序通常是用於排序最佳的實用選擇。7.3節介紹了快速排序的一個隨機化變形,這一版本的平均已耗用時間較好,也沒有什麼特殊的輸入會導致最壞運行狀態。
7.1 快速排序的描述
與合并排序一樣,快速排序也是基於分治模式的。下面是對一個典型子數組A[p...r]排序的分治過程的三個步驟。
分解:數組A[p...r]被劃分成兩個子數組A[p...q-1]和A[q+1,r],使得A[p...q-1]中的每個元素都小於等於A[q],而A[q]小於等於A[q+1...r]中的元素,下標q也在這個劃分過程中進行計算。
解決:通過遞迴調用快速排序,對子數組A[p...q-1]和A[q+1...r]排序
合并:不需要合并
下面的過程實現快速排序:
QUICKSORT(A,p,r)
1 if p<r
2 then q<-- PARTITION(A,p,r)
3 QUICKSORT(A, p, q-1)
4 QUICKSORT(A, q+1, r)
快速排序演算法的關鍵PARTITION過程,它對子數組A[p...r]進行就地重排:
PARTITION(A, p, r)
1 x <-- A[r]
2 i <-- p-1
3 for j<-- p to r-1
4 do if A[j] <= x
5 then i <-- i+1
6 exchange A[j] <--> A[i]
7 exchange A[i+1] <--> A[r]
8 return i+1
上述過程中, 變數i,j將 A[p...r-1]分成了三個區段:
(1) 對p<=k<=i有 A[k]<= x;
(2) 對i<k<j有A[k]>=x;
(3) j<= k <= r-1為尚未比較的未知段。
這三個區段的性質正是上述過程中的迴圈不變式。
7.2快速排序的效能
快速排序的已耗用時間與劃分是否對稱有關,而二者由於與選擇哪個元素來進行劃分有關。如果劃分是對稱的,那麼本演算法從漸進意義上與合并演算法一樣快;如果劃分是不對稱的,那麼就和插入演算法一樣慢。
最壞情況劃分
最壞情況就是劃分過程產生的兩個地區分別包含0和n-1個元素,假設演算法的每一次遞迴調用都產生了這種不對稱的劃分,那麼已耗用時間可遞迴地表示為:
T(n) = T(n-1) + T(0) + Θ(n) = T(n-1) + Θ(n) = Θ(n2)
最佳情況劃分
當劃分產生的兩個地區分別包含 n/2個元素時,產生最佳劃分。此時有
T(n) = T(n/2) + Θ(n) = Θ(nlgn)
平衡的劃分
快速排序的平均效能與最佳情況很接近,而不是接近最差情況。
假設劃分過程總是產生9:1的劃分,乍一看這種劃分很不平衡,這時快速排序已耗用時間可遞迴表示為:
T(n) = T(9n/10) + T(n/10) + Θ(n)
通過遞迴樹可得 T(n) = Θ(nlgn)。
實際上任何一種按常數比例進行劃分都會產生深度為Θ(lgn)的遞迴樹,其中每一層的代價為O(n),其總的已耗用時間都是O(nlgn)。
7.3快速排序的隨機化版本
為了避免特定輸入導致快速排序產生最差的劃分,可以對演算法加入隨機化的成分,以便獲得較好的平均效能。很多人認為快速排序的隨機化版本是對足夠大的輸入的理想化選擇。對快速排序來說,沒有必要像5.3節介紹的那樣對輸入進行隨機化排列,這裡採取一種不同的,稱為隨機取樣的隨機化技術。在這種方法中,不是始終採用A[r]作為主元,而是從子數組A[p...r]中隨機播放一個元素。
與原演算法相比,新的劃分過程如下:
RANDOMIZED-PARTITION(A, p, r)
1 i <-- RANDOM(p,r)
2 exchange A[r] <--> A[i]
3 return PARTITION(A, p, r)
7.4快速排序分析
7.2從直覺上對快速排序的最壞情況、它為何運行得較快作了一些討論,本節要對快速排序效能進行嚴格的分析。先進行最壞情況分析,這對隨機化版本和非隨機化版本都一樣,在分析隨機化版本的平均情況效能。
最壞情況
利用代換法(4.1節)可以證明快速排序最差的已耗用時間為O(n2):
T(n) = max0<=q<=n-1{T(q)+T(n-q-1) }+ Θ(n)
猜測 T(n) <= cn2,c為某個常數。
T(n) <= max0<=q<=n-1{cq2+c(n-q-1)2}+ Θ(n) = c*(q2+(n-q-1)2) + Θ(n) <= cn2-c(2n-1) + Θ(n)
只要選擇足夠大的才c,使得 c(2n-1)可以支配Θ(n), T(n) <= cn2就能成立。
同樣也可以證明 T(n) = Ω(n2);
隨機化的期望效能
快速排序的時間主要是花在PARTITION上的時間決定的,每當PARTITON被調用時,就要選出一個主要元素,後續的遞迴不會再涉及該主要元素,所以整個排序過程中PARTITON最多被調用n次,調用一次PARTITION的時間為O(1)再加上for迴圈中元素比較的次數,如果我們能夠知道總的元素比較次數,就能夠知道快速排序的已耗用時間了。
假設元素比較的次數為X,那麼 演算法的已耗用時間為 O(n+X)。
為了得出元素的比較次數,我們要分析兩個元素何時進行比較,何時不進行比較。為此對元素進行重新命名z1,z2,...,zn,zi為數組中第i小的元素,而且還定義Zij = {zi,...,zj}為zi和zj之間元素的集合。
在PARTITION的過程中,每個元素與主要元素進行比較,之後不會再和主要元素比較,這說明兩個元素最多比較一次。這個特性使我們可以用指標隨機變數來分析問題:
Xij = I{zi和zj進行比較}
那麼演算法何時會比較zi和zj,何時兩個元素不會比較呢?要比較的話,必須有其中一個元素在某個遞迴層次被選為主要元素並且此時兩個元素都還在這個劃分中。某個包含Zij的劃分中有元素 zi<x<zj被選為主要元素,那麼zi和zj就再也沒有機會比較了。所以zi和zj的比較取決於Zij中哪個元素首先被選為主要元素(Zij之外的元素何時被選為主要元素不影響)。
P{Xij=1} = P{zi和zj進行比較} = P{zi或zj在Zij中首先被選為主要元素} = P{zi在Zij中首先被選為主要元素} + P{zj在Zij中首選被選為主要元素} = 2/(j-i+1)。
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 = O(nlgn)
練習:
7..4-5對插入排序來說,當輸入已經“幾乎”排好序時,已耗用時間是很快的,在實踐中可以充分利用這一特點來改善快速排序的已耗用時間,當在一個長度小於k的子數組上來調用快速排序時,讓它不做任何排序就返回。當頂層的快速排序調用返回後,對整個數組進行一次插入排序。證明這一演算法的期望已耗用時間為O(nk+nlg(n/k))。實踐中如何選擇k。
分析:(1)該演算法的已耗用時間由兩部分組成:一是快速排序的時間,而是插入排序的時間。 前者和標準快速排序的時間的唯一區別來說就是遞迴的深度相對較低,參考前面分析演算法執行時間的遞迴樹,標準演算法的遞迴樹的高度為O(lgn),該演算法的遞迴樹高度為O(lgn-lgk)。 因此已耗用時間為O(nlgn/k)。 再看插入排序,假設快速排序最終將數組重新劃分成了A1,A2,...,Am m個區塊,每個區塊的元素個數為k1,k2,...,km, 有ki<=k, ∑ki = n。於是演算法的複雜度為O(k12+k22+...+km2) = O(k1k+k2k+...+kkm) = O(kn)。
(2)我認為實踐中還要找到使插入排序比快速排序塊的k的臨界點。
思考題
7.1 Hoare劃分
本章給出的PARTITION演算法並不是其最初的版本。西面給出Hoare設計的劃分演算法。
HPARE-PARTITION(A, p, r)
1 x <-- A[p]
2 i <-- p-1
3 j <-- r+1
4 while TRUE
5 do repeat j<-- j-1
6 until A[j] <= x
7 do repeat i<--i+1
8 untile A[i]>=x
9 if i<j
10 then A[i]<-->A[j]
11 else return j
HOARE劃分與PARTITION不同,不是將數組劃分成圍繞主元的兩個部分,而是主元也放入了其中一個部分,並保證前一部分的所有元素小於或等於後一部分的所有元素。如果使用HOAR劃分,快速排序的主體過程也要相應修改。
證明上述過程的正確性。
(1)先證明在整個過程中不會訪問到A[p...r]以外的位置,假設P<=r:第一輪迴圈中第6行不會越界,因為至少還有A[p]會使內迴圈停止;同樣的原因同樣第8行也不會越界。在之後的迴圈中,由於有i<j,且A[i]<=x,A[j]>=x。第6行和第8行也不會越界。
(2)再證明迴圈不變式對j<k<=r有A[k]>=x。為了輔助,證明另一有性質:a、在迴圈開始之前有,對j=<k<=r有A[k]>=x。在第一次迴圈開始前,性質a成立。迴圈過程中由於5、6行使得對k>j有A[k]>=x,如果這次迴圈沒有結束的話,那麼亦有A[j]>=x,那麼每次迴圈前a都成立。就已證明不變式在迴圈前成立。 在每次迴圈開始前性質a成立,那麼當該次迴圈結束時5、6行足以保證不變式成立。
同樣可以證明另一不變式:對p=<k<i有A[k]<=x。 當迴圈結束時,i>=j,在結合前面的這些性質。可知A[p...j]中的元素小於或等於A[j+1...r]中的元素。
7.4快速排序中的堆棧深度:消除尾遞迴
QUIKSORT演算法包含兩個對其自身的遞迴調用,其中第二個遞迴並不是必須的,可以考慮用迭代來代替它。這種技術稱作“尾遞迴”。
QUIKSORT' (A, p, r)
1 while (p<r)
2 do q<-- PARTITON(A,p,r)
3 QUIKSORT(A,p,q-1)
4 p<-- q+1
堆棧深度分析:上述過程省去了第二個遞迴導致的棧深度,對第一次遞迴導致的堆棧深度並沒有什麼影響。如果第一次遞迴的總是棧深度比第二次大,那麼這個改動雖然將一個遞迴換成了迭代而提升了一點速度,但並沒有起到減小棧深度的效果。在極端情況下,如果總是有q = r,那麼棧深度為Θ(n)。 為了改進,可以在第一次遞迴前做一個判斷,對元素少的段進行遞迴,對大的段用“尾遞迴”消除。
7.5“三數取中”劃分
一種改進劃分的方法,在選取主要元素時,在三個隨機元素中選擇大小置中的一個。
7.6對區間的模糊排序
給定n個形如[ai,bi]的閉區間,其中ai<=bi。演算法的目標是對這些區間進行模糊排序,產生一個排列(i1,i2,...,in),使得存在一個cj∈[aij,bij],滿足c1<=c2<=...<=cn。設計一個演算法進行排序,演算法應該具有演算法的一般結構,快速排序左部端點(即個ai),也要能充分利用重疊地區改善效能。在一般情況下,演算法的期望的已耗用時間是Θ(nlgn),但當所有的區間都重疊時已耗用時間為Θ(n)。
設計演算法如下:在快速排序的PARTITION過程中,分別記錄兩個劃分的所有區間是否存在共同的重疊區間。如果存在這個劃分就不需要再遞迴了。