快速排序(C語言)-解析


快速排序

快速排序是一種排序算法,對包含 n 個數的輸入數組,最壞情況運行時間為O(n2)。雖然這個最壞情況運行時間比較差,但快速排序通常是用於排序的最佳的實用選擇,

這是因為其平均性能相當好:期望的運行時間為O(nlgn),且O(nlgn)記號中隱含的常數因子很小。另外,它還能夠進行就地排序,在虛存環境中也能很好的工作。

快速排序(Quicksort)是對 冒泡排序的一種改進。
快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然后再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以 遞歸進行,以此達到整個數據變成有序 序列
  像合並排序一樣,快速排序也是采用分治模式的。下面是對一個典型數組A[p……r]排序的分治過程的三個步驟:

分解:

數組 A[p……r]被划分為兩個(可能空)子數組 A[p……q-1] 和 A[q+1……r] ,使得 A[p……q-1] 中的每個元素都小於等於 A(q) , 而且,小於等於 A[q+1……r] 中的元素。小標q也在這個划分過程中進行計算。

解決:

通過遞歸調用快速排序,對於數組 A[p……q-1] 和 A[q+1……r] 排序。

合並:

因為兩個子數組是就地排序的,將它們的合並不需要操作:整個數組 A[p……r] 已排序。

下面的過程實現快速排序(偽代碼):

QUICK SORT(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)

為排序一個完整的數組A,最初的調用是QUICKSORT(A,1,length[A])。

數組划分:

  快速排序算法的關鍵是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[i] <-> A[j]
7 exchange  A[i + 1] <-> A[j]
8 return i+1

排序演示

示例

假設用戶輸入了如下數組:
下標
0
1
2
3
4
5
數據
6
2
7
3
8
9
創建變量i=0(指向第一個數據), j=5(指向最后一個數據), k=6( 賦值為第一個數據的值)。
 
我們要把所有比k小的數移動到k的左面,所以我們可以開始尋找比6小的數,從j開始,從右往左找,不斷遞減變量j的值,我們找到第一個下標3的數據比6小,於是把數據3移到下標0的位置,把下標0的數據6移到下標3,完成第一次比較:
下標
0
1
2
3
4
5
數據
3
2
7
6
8
9
                  i=0 j=3 k=6
接着,開始第二次比較,這次要變成找比k大的了,而且要從前往后找了。遞加變量i,發現下標2的數據是第一個比k大的,於是用下標2的數據7和j指向的下標3的數據的6做交換,數據狀態變成下表:
下標
0
1
2
3
4
5
數據
3
2
6
7
8
9
                  i=2 j=3 k=6
稱上面兩次比較為一個循環。
 
接着,再遞減變量j,不斷重復進行上面的循環比較。
在本例中,我們進行一次循環,就發現i和j“碰頭”了:他們都指向了下標2。於是,第一遍比較結束。得到結果如下,凡是k(=6)左邊的數都比它小,凡是k右邊的數都比它大:
下標
0
1
2
3
4
5
數據
3
2
6
7
8
9
  如果i和j沒有碰頭的話,就遞加i找大的,還沒有,就再遞減j找小的,如此反復,不斷循環。注意判斷和尋找是同時進行的。
  然后,對k兩邊的數據,再分組分別進行上述的過程,直到不能再分組為止。
注意:第一遍快速排序不會直接得到最終結果,只會把比k大和比k小的數分到k的兩邊。為了得到最后結果,需要再次對下標2兩邊的數組分別執行此步驟,然后再分解數組,直到數組不能再分解為止(只有一個數據),才能得到正確結果。

 

1)設置兩個變量i、j,排序開始的時候:i=0,j=N-1;
 
2)以第一個數組元素作為關鍵數據,賦值給key,即key=A[0];
 
3)從j開始向前搜索,即由后開始向前搜索(j--),找到第一個小於key的值A[j],將A[j]和A[i]互換;
 
4)從i開始向后搜索,即由前開始向后搜索(i++),找到第一個大於key的A[i],將A[i]和A[j]互換;
 
5)重復第3、4步,直到i=j; (3,4步中,沒找到符合條件的值,即3中A[j]不小於key,4中A[i]不大於key的時候改變j、i的值,使得j=j-          1,i=i+1,直至找到為止。找到符合條件        的值,進行交換的時候i, j指針位置不變。另外,i==j這一過程一定正好是i+或j-完成      的時候,此時令循環結束)。
 
代碼(C語言):
 1 #include<stdio.h>
 2 
 3 
 4 void Quick_Sort(int *a, int left, int right) //方法 
 5 {
 6     if(left >= right)/*如果左邊索引大於或者等於右邊的索引就代表已經整理完成一個組了*/
 7         return ;
 8     int i = left;
 9     int j = right;
10     int key = a[left];
11     while( i < j)    /*控制在當組內尋找一遍*/
12     {
13         while(i < j && key <= a[j]) 
14         /*而尋找結束的條件就是,1,找到一個大於key的數(大於或小於取決於你想升
15         序還是降序,這里是升序)2,沒有符合條件1的,並且i與j的大小沒有反轉*/ 
16         {
17             j--;
18         }
19         a[i] = a[j];
20         while(i < j && key >= a[i])
21         /*這是i在當組內向前尋找,同上,不過注意與key的大小關系停止循環和上面相反,
22         因為排序思想是把數往兩邊扔,所以左右兩邊的數大小與key的關系相反*/
23         {
24             i++;
25         }
26         a[j] = a[i];        
27     }
28     a[i] = key;/*當在當組內找完一遍以后就把中間數key回歸*/
29     Quick_Sort(a, left, i - 1);/*最后用同樣的方式對分出來的左邊的小組進行同上的做法*/
30     Quick_Sort(a, i +1, right); /*用同樣的方式對分出來的右邊的小組進行同上的做法*/
31 } 
32 int main()
33 {
34     int a[10];
35     int i,j,t;
36     printf("input 10 numbers :\n");//輸入 
37     for(i = 0;i < 10; i++)         //數組 
38         scanf("%d",&a[i]);         //a[0]~a[9] 
39     printf("\n");
40     void Quick_Sort(int *a, int left, int right);
41     Quick_Sort(a, 0, 9);    
42     printf("the sorted numbers :\n");
43     for(i = 0;i < 10; i++)      //輸出 
44         printf("%d ",a[i]);     //數組 
45     printf("\n");                
46     return 0;
47 }

輸入/輸出:

 

歷史:

快速排序由 C. A. R. Hoare(東尼霍爾,Charles Antony Richard Hoare)在1960年提出,之后又有許多人做了進一步的優化。如果你對快速排序感興趣可以去看看東尼霍爾1962年在Computer Journal發表的論文“Quicksort”以及《算法導論》的第七章。快速排序算法僅僅是東尼霍爾在計算機領域才能的第一次顯露,后來他受到了老板的賞識和重用,公司希望他為新機器設計一個新的高級語言。你要知道當時還沒有PASCAL或者C語言這些高級的東東。后來東尼霍爾參加了由Edsger Wybe Dijkstra(1972年圖靈獎得主,這個大神我們后面還會遇到的到時候再細聊)舉辦的“ALGOL 60”培訓班,他覺得自己與其沒有把握去設計一個新的語言,還不如對現有的“ALGOL 60”進行改進,使之能在公司的新機器上使用。於是他便設計了“ALGOL 60”的一個子集版本。這個版本在執行效率和可靠性上都在當時“ALGOL 60”的各種版本中首屈一指,因此東尼霍爾受到了國際學術界的重視。后來他在“ALGOL X”的設計中還發明了大家熟知的“case”語句,后來也被各種高級語言廣泛采用,比如PASCAL、C、Java語言等等。當然,東尼霍爾在計算機領域的貢獻還有很多很多,他在1980年獲得了圖靈獎。

優化:

三平均分區法

  關於這一改進的最簡單的描述大概是這樣的:與一般的快速排序方法不同,它並不是選擇待排數組的第一個數作為中軸,而是選用待排數組最左邊、最右邊和最中間的三個元素的中間值作為中軸。這一改進對於原來的快速排序算法來說,主要有兩點優勢:
  (1) 首先,它使得最壞情況發生的幾率減小了。
  (2) 其次,未改進的快速排序算法為了防止比較時數組越界,在最后要設置一個哨點。
 

根據分區大小調整算法

  這一方面的改進是針對快速排序算法的弱點進行的。快速排序對於小規模的數據集性能不是很好。可能有人認為可以忽略這個缺點不計,因為大多數排序都只要考慮大規模的適應性就行了。但是快速排序算法使用了分治技術,最終來說大的數據集都要分為小的數據集來進行處理。由此可以得到的改進就是,當數據集較小時,不必繼續遞歸調用快速排序算法,而改為調用其他的對於小規模數據集處理能力較強的排序算法來完成。Introsort就是這樣的一種算法,它開始采用快速排序算法進行排序,當遞歸達到一定深度時就改為堆排序來處理。這樣就克服了快速排序在小規模數據集處理中復雜的中軸選擇,也確保了堆排序在最壞情況下O(n log n)的復雜度。

  另一種優化改進是當分區的規模達到一定小時,便停止快速排序算法。也即快速排序算法的最終產物是一個“幾乎”排序完成的有序數列。數列中有部分元素並沒有排到最終的有序序列的位置上,但是這種元素並不多。可以對這種“幾乎”完成排序的數列使用插入排序算法進行排序以最終完成整個排序過程。因為插入排序對於這種“幾乎”完成的排序數列有着接近線性的復雜度。這一改進被證明比持續使用快速排序算法要有效的多。

  另一種快速排序的改進策略是在遞歸排序子分區的時候,總是選擇優先排序那個最小的分區。這個選擇能夠更加有效的利用存儲空間從而從整體上加速算法的執行。

不同的分區方案考慮

  對於快速排序算法來說,實際上大量的時間都消耗在了分區上面,因此一個好的分區實現是非常重要的。尤其是當要分區的所有的元素值都相等時,一般的快速排序算法就陷入了最壞的一種情況,也即反復的交換相同的元素並返回最差的中軸值。無論是任何數據集,只要它們中包含了很多相同的元素的話,這都是一個嚴重的問題,因為許多“底層”的分區都會變得完全一樣。
  對於這種情況的一種改進辦法就是將分區分為三塊而不是原來的兩塊:一塊是小於中軸值的所有元素,一塊是等於中軸值的所有元素,另一塊是大於中軸值的所有元素。另一種簡單的改進方法是,當分區完成后,如果發現最左和最右兩個元素值相等的話就避免遞歸調用而采用其他的排序算法來完成。
 

並行的快速排序

  由於快速排序算法是采用分治技術來進行實現的,這就使得它很容易能夠在多台處理機上並行處理。
  在大多數情況下,創建一個線程所需要的時間要遠遠大於兩個元素比較和交換的時間,因此,快速排序的並行算法不可能為每個分區都創建一個新的線程。一般來說,會在實現代碼中設定一個閥值,如果分區的元素數目多於該閥值的話,就創建一個新的線程來處理這個分區的排序,否則的話就進行遞歸調用來排序。
  對於這一並行快速排序算法也有其改進。該算法的主要問題在於,分區的這一步驟總是要在子序列並行處理之前完成,這就限制了整個算法的並行程度。解決方法就是將分區這一步驟也並行處理。改進后的並行快速排序算法使用2n個指針來並行處理分區這一步驟,從而增加算法的並行程度。

 

變種

隨機化快排

快速排序的最壞情況基於每次划分對主元的選擇。基本的快速排序選取第一個元素作為主元。這樣在 數組已經有序的情況下,每次划分將得到最壞的結果。一種比較常見的優化方法是隨機化算法,即隨機選取一個元素作為主元。這種情況下雖然最壞情況仍然是O(n^2),但最壞情況不再依賴於輸入數據,而是由於 隨機函數取值不佳。實際上,隨機化快速排序得到理論最壞情況的可能性僅為1/(2^n)。所以隨機化快速排序可以對於絕大多數輸入數據達到O(nlogn)的期望 時間復雜度。一位前輩做出了一個精辟的總結:“隨機化快速排序可以滿足一個人一輩子的人品需求。”
隨機化快速排序的唯一缺點在於,一旦輸入數據中有很多的相同數據,隨機化的效果將直接減弱。對於極限情況,即對於n個相同的數排序,隨機化快速排序的時間復雜度將毫無疑問的降低到O(n^2)。解決方法是用一種方法進行掃描,使沒有交換的情況下主元保留在原位置。
 

平衡快排

每次盡可能地選擇一個能夠代表中值的元素作為關鍵數據,然后遵循普通快排的原則進行比較、替換和 遞歸。通常來說,選擇這個數據的方法是取開頭、結尾、中間3個數據,通過比較選出其中的中值。取這3個值的好處是在實際問題中,出現近似順序數據或逆序數據的概率較大,此時中間數據必然成為中值,而也是事實上的近似中值。萬一遇到正好中間大兩邊小(或反之)的數據,取的值都接近最值,那么由於至少能將兩部分分開,實際效率也會有2倍左右的增加,而且利於將數據略微打亂,破壞退化的結構。
 

外部快排

與普通快排不同的是,關鍵數據是一段buffer,首先將之前和之后的M/2個元素讀入buffer並對該buffer中的這些元素進行排序,然后從被 排序 數組的開頭(或者結尾)讀入下一個元素,假如這個元素小於buffer中最小的元素,把它寫到最開頭的空位上;假如這個元素大於buffer中最大的元素,則寫到最后的空位上;否則把buffer中最大或者最小的元素寫入數組,並把這個元素放在buffer里。保持最大值低於這些關鍵數據,最小值高於這些關鍵數據,從而避免對已經有序的中間的數據進行重排。完成后,數組的中間空位必然空出,把這個buffer寫入數組中間空位。然后 遞歸地對外部更小的部分,循環地對其他部分進行排序。
 

三路基數快排

(Three-way Radix Quicksort,也稱作Multikey Quicksort、Multi-key Quicksort):結合了 基數排序(radix sort,如一般的 字符串比較排序就是基數排序)和快排的特點,是字符串排序中比較高效的算法。該算法被排序 數組的元素具有一個特點,即multikey,如一個字符串,每個字母可以看作是一個key。算法每次在被排序數組中任意選擇一個元素作為關鍵數據,首先僅考慮這個元素的第一個key(字母),然后把其他元素通過key的比較分成小於、等於、大於關鍵數據的三個部分。然后 遞歸地基於這一個key位置對“小於”和“大於”部分進行排序,基於下一個key對“等於”部分進行排序。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM