快速排序的基本思想是,通過一輪的排序將序列分割成獨立的兩部分,其中一部分序列的關鍵字(這里主要用值來表示)均比另一部分關鍵字小。繼續對長度較短的序列進行同樣的分割,最后到達整體有序。在排序過程中,由於已經分開的兩部分的元素不需要進行比較,故減少了比較次數,降低了排序時間。
詳細描述:首先在要排序的序列 a 中選取一個中軸值,而后將序列分成兩個部分,其中左邊的部分 b 中的元素均小於或者等於 中軸值,右邊的部分 c 的元素 均大於或者等於中軸值,而后通過遞歸調用快速排序的過程分別對兩個部分進行排序,最后將兩部分產生的結果合並即可得到最后的排序序列。
“基准值”的選擇有很多種方法。最簡單的是使用第一個記錄的關鍵字值。但是如果輸入的數組是正序或者逆序的,就會將所有的記錄分到“基准值”的一邊。較好的方法是隨機選取“基准值”,這樣可以減少原始輸入對排序造成的影響。但是隨機選取“基准值”的開銷大。
為了實現一次划分,我們可以從數組(假定數據是存在數組中)的兩端移動下標,必要時交換記錄,直到數組兩端的下標相遇為止。為此,我們附設兩個指針(下角標)i 和 j, 通過 j 從當前序列的有段向左掃描,越過不小於基准值的記錄。當遇到小於基准值的記錄時,掃描停止。通過 i 從當前序列的左端向右掃描,越過小於基准值的記錄。當遇到不小於基准值的記錄時,掃描停止。交換兩個方向掃描停止的記錄 a[j] 與 a[i]。 然后,繼續掃描,直至 i 與 j 相遇為止。掃描和交換的過程結束。這是 i 左邊的記錄的關鍵字值都小於基准值,右邊的記錄的關鍵字值都不小於基准值。
通過兩個不相鄰元素交換,可以一次交換消除多個逆序,加快排序速度。快速排序方法在要排序的數據已經有序的情況下最不利於發揮其長處。
下面我們通過一個案例來演示一下快速排序的基本步驟: 以序列 46 30 82 90 56 17 95 15 共8個元素
初始狀態: 46 30 82 90 56 17 95 15 選擇46 作為基准值,i = 0, j = 7
i = 0 j = 7
15 30 82 90 56 17 95 46 15 < 46, 交換 15 和 46,移動 i, i = 1
i = 1 j = 7
15 30 82 90 56 17 95 46 30 < 46, 不需要交換,移動 i , i = 2
i = 2 j = 7
15 30 46 90 56 17 95 82 82 > 46, 交換82 和 46,移動 j , j = 6
i = 2 j = 6
15 30 46 90 56 17 95 82 95 > 46, 不需要交換,移動 j , j = 5
i = 2 j = 5
15 30 17 90 56 46 95 82 17 < 46, 交換46 和 17,移動 i, i = 3
i = 3 j = 5
15 30 17 46 56 90 95 82 90 > 46, 交換90 和 46,移動 j , j = 4
3 = i j = 4
15 30 17 46 56 90 95 82 56 > 46, 不需要交換,移動 j , j = 3
i = j = 3
i = j = 3, 這樣序列就這樣分割成了兩部分,左邊部分{15, 30, 17} 均小於 基准值(46);右邊部分 {56, 90,95,82},均大於基准值。這樣子我們就達到了分割序列的目標。在接着對子序列用同樣的辦法進行分割,直至子序列不超過一個元素,那么排序結束,整個序列處於有序狀態。
參考代碼:

1 #include <stdio.h> 2 3 #define MAX_NUM 80 4 5 void quicksort(int* a, int p,int q) 6 { 7 int i = p; 8 int j = q; 9 int temp = a[p]; 10 11 while(i < j) 12 { 13 // 越過不小於基准值的數據 14 while( a[j] >= temp && j > i ) j--; 15 16 if( j > i ) 17 { 18 a[i] = a[j]; 19 i++; 20 21 // 越過小於基准值的數據 22 while(a[i] <= temp && i < j ) i++; 23 if( i < j ) 24 { 25 a[j] = a[i]; 26 j--; 27 } 28 } 29 } 30 a[i] = temp; 31 32 for(int k = p; k <= q;k++) 33 { 34 if( k == i ) 35 { 36 printf("(%d) ",a[k]); 37 continue; 38 } 39 printf("%d ",a[k]); 40 } 41 printf("\n"); 42 43 if( p < (i-1)) quicksort(a,p,i-1); 44 if((j+1) < q ) quicksort(a,j+1,q); 45 } 46 47 int main(int argc, char* argv[]) 48 { 49 int a[MAX_NUM]; 50 int n; 51 52 printf("Input total numbers: "); 53 scanf("%d",&n); 54 55 if( n > MAX_NUM ) n = MAX_NUM; 56 57 for(int i = 0; i < n;i++) 58 { 59 scanf("%d",&a[i]); 60 } 61 62 printf("Divide sequence:\n"); 63 quicksort(a,0,n-1); 64 65 printf("The sorted result:\n"); 66 for(int i = 0; i < n;i++) 67 { 68 printf("%d ",a[i]); 69 } 70 printf("\n"); 71 72 73 74 return 0; 75 }
案例運行結果:(注:括號內表示的是基准值)
快速排序算法效率與穩定性分析
當基數值不能很好地分割數組,即基准值將數組分成一個子數組中有一個記錄,而另一個子組組有 n -1 個記錄時,下一次的子數組只比原來數組小 1,這是快速排序的最差的情況。如果這種情況發生在每次划分過程中,那么快速排序就退化成了冒泡排序,其時間復雜度為O(n2)。
如果基准值都能講數組分成相等的兩部分,則出現快速排序的最佳情況。在這種情況下,我們還要對每個大小約為 n/2 的兩個子數組進行排序。在一個大小為 n 的記錄中確定一個記錄的位置所需要的時間為O(n)。若T(n)為對n個記錄進行排序所需要的時間,則每當一個記錄得到其正確位置,整組大致分成兩個相等的兩部分時,我們得到快速排序算法的最佳時間復雜性。
T(n) <= cn + 2T(n/2) c是一個常數
<= cn + 2(cn/2+2T(n/4)) = 2cn+ 4T(n/4)
<= 2cn + 4(cn/4+ 2T(n/8)) = 3cn + 8T(n/8)
…… ……
<= cnlogn + nT(1) = O(nlogn) 其中cn 是一次划分所用的時間,c是一個常數
最壞的情況,每次划分都得到一個子序列,時間復雜度為:
T(n) = cn + T(n-1)
= cn + c(n-1) + T(n - 2) = 2cn -c + T(n-2)
= 2cn -c + c(n - 2) + T(n-3) = 3cn -3c + T(n-3)
……
= c[n(n+1)/2-1] + T(1) = O(n2)
快速排序的時間復雜度在平均情況下介於最佳與最差情況之間,假設每一次分割時,基准值處於最終排序好的位置的概率是一樣的,基准值將數組分成長度為0 和 n-1,1 和 n-2,……的概率都是 1/n。在這種假設下,快速排序的平均時間復雜性為:
T(n) = cn + 1/n(T(k)+ T(n-k-1)) T(0) = c, T(1) = c
這是一個遞推公式,T(k)和T(n-k-1)是指處理長度為 k 和 n-k-1 數組是快速排序算法所花費的時間, 根據公式所推算出來的時間為 O(nlogn)。因此快速排序的平均時間復雜性為O(nlogn)。
快速排序需要棧空間來實現遞歸,如果數組按局等方式被分割時,則最大的遞歸深度為 log n,需要的棧空間為 O(log n)。最壞的情況下在遞歸的每一級上,數組分割成長度為0的左子數組和長度為 n - 1 的右數組。這種情況下,遞歸的深度就成為 n,需要的棧空間為 O(n)。
因為快速排序在進行交換時,只是根據比較基數值判斷是否交換,且不是相鄰元素來交換,在交換過程中可能改變相同元素的順序,因此是一種不穩定的排序算法。
注:主要參考彭軍、向毅主編的 《數據結構與算法》