演算法導論 第七章:快速排序 筆記(快速排序的描述、快速排序的效能、快速排序的隨機化版本、快速排序分析)
快速排序的最壞情況時間複雜度為Θ(n^2)。雖然最壞情況時間複雜度很差,但是快速排序通常是實際排序應用中最好的選擇,因為它的平均效能很好。它的期望執行時間複雜度為Θ(n lg n),而且Θ(n lg n)中蘊含的常數因子非常小,而且它還是原址排序的。
快速排序是一種排序演算法,對包含n個數的輸人陣列,最壞情況執行時間為Θ(n^2) 。雖然這個最壞情況執行時間比較差,但快速排序通常是用於排序的最佳的實用選擇,這是因為其平均效能相當好:期望的執行時間為Θ(nlgn), 且Θ(nlgn)記號中隱含的常數因子很小。另外, 它還能夠進行就地排序,在虛存環境中也能很好地工作。
快速排序的描述:
快速排序也採用分治法進行排序,首先在陣列中選擇一個元素P,根據元素P將陣列劃分為兩個子陣列,在元素P左側的子陣列的所有元素都小於或等於該元素P,右側的子陣列的所有元素都大於元素P。
下面是對一個典型子陣列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]排序。
合併:
因為兩個子陣列是就地排序的,將它們的合併不需要操作:整個陣列A [ p.. r ] 己排序。
虛擬碼:
//快速排序 QUICKSORT(A,p,r) if p < r //當陣列中只剩一個元素時,退出遞迴,陣列已經有序 q <- PARTITION(A,p,r) QUICKSORT(A,p,q-1) //對小於等於A[q]的元素進行遞迴 QUICKSORT(A,q+1,r) //對大於A[q]的元素進行遞迴 //陣列劃分 PARTITION(A,p,r) x <- A[r] //將A[r]選取為“主元” i <- p - 1 //因為可能不存在小於等於A[r]的元素,所以i的值由p-1開始 for j <- p to r-1 if A[j] <= x i <- i + 1 //小於等於x的元素增加一個 exchange A[i] <-> A[j] //i+1後,i指向了一個大於x的元素,此時,j指向的是一個小於等於x的元素,交換這兩個元素的位置,使其符合規則 exchange A[i+1] <-> A[r] //r前所有元素比較完後,將A[r]置於正確位置:兩個子陣列的交界處 return i + 1 //返回"主元"的位置
如:
習題:
7.1-2:
當陣列A[p.. r] 中的元素均相同時, PARTITION返回的q值是什麼?修改PARTITION,使得當陣列A[p.. r] 中所有元素的值相同時, q = (p+r)/2 。
返回最後一個元素r 。
修改PARTITION:
PARTITION_1(A,p,r) x <- A[r] i <- p - 1 count <- 0 //加入一個計數器 for j <- p to r - 1 if A[j]<= x if A[i] == x count = count + 1 i <- i + 1 exchange A[i] <-> A[j] if count == r - p + 1 return (p + r)/2 exchange A[i + 1] <-> A[r] return i + 1
快速排序的效能:
快速排序的執行時間依賴於劃分是否平衡,如果劃分平衡,那麼快速排序演算法效能與歸併排序一樣,如果劃分不平衡,那麼快速排序的效能就接近於插入排序了。
最壞情況劃分(不平衡):
最壞情況下,每次劃分的兩個子問題都分別包含了n-1個元素和0個元素。劃分操作的時間複雜度是 Θ(n),因為對一個大小為0的陣列進行遞迴呼叫後,返回了T(n)=O(1),故演算法的執行時間可遞迴的表示為:
T(n) = T(n-1) + T(0) + Θ(n) =T(n-1) + Θ(n)
該遞迴式的解為:T(n) =Θ(n^2)。因此,最壞情況下,也就是陣列中元素已經排好序的時候,快速排序的時間複雜度為Θ(n^2),而在同樣的情況下,插入排序的時間複雜度為O(n)。
最好情況劃分(最平衡):
最好的情況下,每次劃分都是平均的劃分為n/2個元素子陣列,此時遞迴式為:
T(n) = 2T(n/2) +T(0) + Θ(n)
該式的解為T(n) = Θ(nlgn)。
快速排序的平均執行時間更接近於其最好情況,而非最壞情況。
事實上,任何一種常數比例的劃分都會產生深度為Θ(nlgn)的遞迴樹,其中每一層的代價都是O(n),因此,只要劃分是常數比例的,演算法的執行時間總是O(nlgn)。
如:
例如,假設劃分過程總是產生9 : 1 的劃分,乍一看這種劃分很不平衡,這時,快速排序執行時間的遞迴式為
T(n) <= T(9n/10) + T(n/10) + cn
該樹每一層的代價都是cn, 直到在深度處達到邊界條件時為止,在此之下各層的代價至多為cn。遞歸於深度處終止。這樣,快速排序的總代價為O(nlgn) 。
當資料量很小的時候,比如只有十幾個元素的小型序列,快排的優勢並不明顯,甚至比插入排序慢。但是一旦資料多,它的優勢就充分發揮出來了。
C++ STL 中的sort函式,就充分發揮了快排的優勢,並且取長補短,在資料量大時採用QuickSort,分段遞迴排序。一旦分段後的資料量小於某個門檻,為避免QuickSort遞迴呼叫帶來過大的額外負荷,就改用插入排序。如果遞迴層次過深,還會改用HeapSort(堆排序)。所以說,C++的“混合兵種”sort的效能肯定會比C的qsort好。
習題:
7.2-1:
利用代換法證明: 遞迴式的解為,如第7. 2 節開頭提到的那樣。
這裡只證明上界,下界可類似證明。
猜測
綜合上下界有T(n)=Θ(n2)。
7.2-2:
當陣列A 的所有元素都具有相同值時, QUICKSORT 的執行時間是什麼?
當陣列A的所有元素都具有相同值時,此時出現最壞情況,每次劃分會出現大小為0和n−1的子陣列,此時時間複雜度為Θ(n2)。
快速排序的隨機化版本:
當輸入的資料是隨機排列的時候,快速排序的時間複雜度是O(nlgn)。但是在實際中,輸入並不總是隨機的,因此需要在演算法中引入隨機性,可以對輸入進行重新排列是演算法實現隨機化, 也可以進行隨機抽樣,隨機抽樣是從陣列A[p…r]中隨機選擇一個元素作為主元。
虛擬碼:
Randomized-Partition(A, p, r)
i <- Random(p, r)
exchange A[r] <-> A[i]
return Partition(A, p, r)
Randomized-Quick-sort(A, p, r)
if p < r
q <- Randomized-Partition(A, p, r)
Randomized-Quick-sort(A, p, q-1)
Randomized-Quick-sort(A, q+1, r)
使用尾遞迴優化快速排序:
傳統的遞迴演算法在很多時候被視為洪水猛獸. 它的名聲狼籍, 好像永遠和低效聯絡在一起,尾遞迴是極其重要的,不用尾遞迴,函式的堆疊耗用難以估量,需要儲存很多中間函式的堆疊。
QUICKSORT演算法包含兩個對其自身的遞迴呼叫,即呼叫PARTITION後,左邊的子陣列和右邊的子陣列分別被遞迴排序。
QUICKSORT中的第二次遞迴呼叫並不是必須的,可以用迭代控制結構來代替它,這種技術叫做“尾遞迴”,大多數的編譯器也使用了這項技術。
下面的虛擬碼模擬了尾遞迴:
QUICKSORT'(A, p, r)
while p < r
do ▸ Partition and sort left subarray.
q <- PARTITION(A, p, r)
QUICKSORT'(A, p, q - 1)
p <- q + 1
注意第一行是while而不是if。
上面的版本在最壞的情況下,就是劃分不好的時候,遞迴深度為O(n)。
我們還可以進一步優化,用二分思想,為了使最壞情況下棧的深度為Θ(lgn),我們必須讓PARTITION後左邊的子陣列為原來陣列的一半大小,這樣遞迴的深度最多為Θ(lgn)。
一種處理方案:
首先求得(A, p, r)的中位數,作為PARTITION的樞軸元素,這樣可以保證左右兩邊的元素的個數儘可能的均衡。
因為求中位數的過程MEDIAN的時間複雜度為Θ(n),因此可以保證演算法的期望的時間複雜度O(nlgn)不變。
優化後的尾遞迴快排:
QUICKSORT (A, p, r)
while p < r
do Partition and sort the small subarray Prst
q <- PARTITION(A,p,r)
if q - p < r - q
then QUICKSORT (A, p, q - 1)
p <- q + 1
else QUICKSORT (A, q + 1,r)
r <- q - 1
除此之外,快排還可以有優化:
三數取中:
從子陣列中隨機選出三個元素,取其中間數作為主元,不過這隻能影響快速排序時間複雜度O(nlgn)的常數因子。
非遞迴方法:
即模擬遞迴,這樣可以完全消去遞迴的呼叫。
三劃分快速排序:
基本思想是,在劃分階段以V=A[r]為基準,將帶排序陣列A[p..r]劃分為左、中、右三段A[p,j],A[j+1..q-1],A[q..r],其中左段陣列元素值小於V,中斷陣列等於V,有段陣列元素大於V。其後,演算法對左右兩段陣列遞迴排序。 這個方法對於有大量相同資料的陣列排序效率有很大的提高,即使沒有大量相同元素,也不降低原快排演算法的效率。
快速排序分析:
利用RANDOMIZED-PARTITION,快速排序演算法期望的執行時間當元素值不同時,為O(nlgn)。
習題:
7.4-1:
證明:在遞迴式:中,T(n)=Ω(n^2)。
使用代換法。
猜測 T(n) <= c*n^2,c為某個常數。
選擇足夠大的c,使得 c*(2*n-1)可以支配Θ(n), T(n) <= c*n^2成立。
7.4-4:
證明:RANDOMIZED-QUICKSORT 演算法的期望執行時間是Ω(nlgn)。