快速排序,對於n個數的輸入數組,最壞情況運行時間:Θ(n^2);期望運行時間:Θ(nlgn);就地排序(Sort in place)。
數組A[p..r]會被分為兩個子數組A[p..q-1]和A[q+1..r],其中A[p..q-1]的元素都不大於A[q],A[q+1..r]都不小於A[q]。
如何划分子數組對運行時間的有很大影響,最壞的情況為n個數的數組划分為一個n-1的數組和一個0元素的數組;最佳情況為對半分(1:1);對於平均情況的划分,其運行時間與最佳情況很接近。
先看代碼實現:
快速排序的關鍵是數組划分,對子數組進行就地重排。
1 /* 2 * 調用划分函數,使得子數組順序重排 3 */ 4 void quick_sort(int A[], int p, int r){ 5 if(p < r){ 6 int q = partition(A, p, r); 7 quick_sort(A, p, q-1); 8 quick_sort(A, q+1, r); 9 } 10 } 11 12 /* 13 * 對於比x值小的元素通過交換放置到小於x值的區域。 14 * 最后將大於x值的區域的第一個元素與x值,即原A[r],交換 15 * 該下標即為q,形成兩個符合要求的數組(A[p..q-1]的元素都不大於A[q],A[q+1..r]都不小於A[q]) 16 */ 17 int partition(int A[], int p, int r){ 18 int x = A[r]; 19 int i = p - 1; 20 int j; 21 for(j = p; j <= r-1; j++){ 22 if(A[j] <= x){ 23 i++; 24 int temp = A[i]; 25 A[i] = A[j]; 26 A[j] = temp; 27 } 28 } 29 int temp = A[i+1]; 30 A[i+1] = A[r]; 31 A[r] = temp; 32 return i+1; 33 }
其中partition的運行時間為Θ(n),n = r-p+1,循環的次數為r-1-p;
過程圖如下:

練習7.1-2
當數組A[p..r]中的元素均相同時,partition返回的q值是多少?修改partition,使得數組A[p..r]中的元素均相同時, q = floor((p+r)/2)
當數組A[p..r]中的元素均相同時,partition返回的q值是r;修改很簡單,直接判斷返回的q值是否與r相同,相同就返回 floor((p+r)/2)。
經網友提醒這種方法判斷元素是否全部相同存在錯誤。。。。。日后再補上。。。囧。。。。
快速排序的性能
快速排序的運行時間與划分數組是否對稱有關。如果划分是對稱,從漸近意義上講,與合並排序一樣快,否則就與插入排序一樣慢。
最壞情況為划分一個n-1個元素的子數組和一個0個元素的子數組。(最大程度不對稱)。划分運行時間為Θ(n)。如果對一個0個元素的數組進行遞歸調用,運行時間為Θ(1).
遞歸式為:T(n) = T(n-1) + T(0) + Θ(n) = T(n-1) + Θ(n)。通過代換法可證明T(n) = Θ(n^2) (練習7.2-1)
證明:先假設C1(n-1)^2 <= T(n-1) <= C2(n-1)^2成立
則C1(n-1)^2 + Θ(n) <= T(n-1) + Θ(n) <= C2(n-1)^2 + Θ(n)
C1n^2 -C1(2n - 1) + Θ(n) <= T(n-1) + Θ(n) <= C2n^2 -C2(2n - 1) + Θ(n)
我們可以選擇合適的常數C1、C2使得C1(2n - 1)、C2(2n - 1)支配Θ(n)。因此可以得出T(n) = Θ(n^2)。
注意,當輸入數組完全排好序(升降序一樣)和所有元素都是相同的值時,運行時間為Θ(n^2),因為每次划分都是最大程度不對稱。畫畫圖,過一遍就會知道。
最佳情況為划分的兩個子數組大小為floor(n/2)和ceiling(n/2)-1。
遞歸式為:T(n) <= 2T(n/2) + Θ(n)。該遞歸式的解為:T(n) = O(nlgn)
直接通過主定理的情況2得出。
平均情況運行時間更接近最佳情況而不是最壞情況。任一種按常數進行划分都會產生深度為Θ(lgn)的遞歸書,每一層的總代價為O(n),因此運行時間為O(nlgn)。
從下圖看可以更加明顯

最壞情況的划分之后最佳情況划分的總代價與直接最佳情況的總代價都為Θ(n)。
練習7.2-5
假設快速排序的每一層,所做划分比例都是1-a:a,其中0<a<=1/2是個常數。證明:在對應的遞歸樹中,葉結點的最小深度大約是-lgn/lga,最大深度大約是-lgn/lg(1-a)。
最小深度為每次划分后都是選擇最小的一部分繼續往下走,每次乘以a。一次迭代減少的元素數從n到an,迭代m次直到剩下的元素為1。
則(a^m)*n = 1, a^m = 1/n,取對數得mlga = -lgn,m = -lgn/lga。
同理可得((1-a)^M)*n = 1,M = -lgn/lg(1-a)。
快速排序隨機化版本
便於對於所有輸入,均能獲得較好的平均情況性能。
1 int randomized_partition(int A[], int p, int r){ 2 int i = p + rand() % (r - p + 1); 3 int temp = A[r]; 4 A[r] = A[i]; 5 A[i] = temp; 6 return partition(A, p, r); 7 } 8 9 int partition(int A[], int p, int r){ 10 int x = A[r]; 11 int i = p - 1; 12 int j; 13 for(j = p; j <= r-1; j++){ 14 if(A[j] <= x){ 15 i++; 16 int temp = A[i]; 17 A[i] = A[j]; 18 A[j] = temp; 19 } 20 } 21 int temp = A[i+1]; 22 A[i+1] = A[r]; 23 A[r] = temp; 24 return i+1; 25 } 26 27 void randomized_quick_sort(int A[], int p, int r){ 28 if(p < r){ 29 int q = randomized_partition(A, p, r); 30 randomized_quick_sort(A, p, q-1); 31 randomized_quick_sort(A, q+1, r); 32 } 33 }
練習7.3-2
在randomized-quicksort的運行過程中,最壞情況下對隨機數產生器random調用了多少次?最佳情況調用了多少次?
都為Θ(n)。
快速排序的分析
算導關於快排分析得很詳細,數學太差了,看了很多遍才明白一點點,日后一定要繼續努力,爭取用自己的語言表達出來。
練習7.4-2
證明:快速排序的最佳情況運行時間為Ω(nlgn)




Hoare划分快速排序
划分方式有些不一樣;前面的partition划分是將主元值與圍繞它划分形成的兩部分分隔開來。而Hoare划分則總是將主元值放入到兩個划分的子數組里。
1 int hoare_partition(int A[], int p, int r){ 2 int x = A[p]; 3 int i = p; 4 int j = r; 5 6 while(i < j){ 7 while(i < j && A[j] > x) 8 j--; 9 A[i] = A[j]; 10 while(i < j && A[i] < x) 11 i++; 12 A[j] = A[i]; 13 } 14 A[i] = x; 15 return j; 16 } 17 18 void hoare_quick_sort(int A[], int p, int r){ 19 if(p < r){ 20 int q = hoare_partition(A, p, r); 21 hoare_quick_sort(A, p, q-1); 22 hoare_quick_sort(A, q+1, r); 23 } 24 }
繼續努力。。。
