最快排序——快速排序(算法三)

​提高效率的本質在於少作事。算法

一. 序

連續兩篇文章講排序,不少讀寫都火燒眉毛的問,有沒有最好的排序算法?這個其實很難回答,成年人的世界歷來就沒有所謂的最好。有句惟美的話:愛一我的就像在海灘上撿貝殼,不要撿最大的,也不要撿最漂亮的,就撿你最喜歡的那一個,撿到了,就永遠不要再去海灘。爲何呢?由於海灘上永遠還有更好的貝殼。好壞都是有條件的,戰場上對敵人如何殘忍都不算壞,小人的諂媚如何的順耳也算不上好。編程

 

綜合來說,在計算機科學中,一般認爲最好的排序算法是託尼·霍爾(Tony Hoare)發明的快速排序(Quicksort)算法。這位託尼·霍爾還所以得到了爵士頭銜,因而可知該算法的重要性和精妙程度。數組

 

託尼·霍爾在前蘇聯莫斯科國立大學作訪問學生時,爲了解決俄文排序問題,他首先嚐試了插入排序,可是因爲不滿插入排序的效率,他想出了快速排序的方法。大神的世界就是本身給本身發明工具。高德納爲了寫書而開發了印刷行業最好的排版軟件,牛頓爲了解決加速度等問題而發明了微積分。可是霍爾當時沒有掌握支持遞歸的編程語言,因此一直沒有實現該算法。直到後來回到英國,學習了ALGOL(支持遞歸)語言,他才把本身的想法付諸實踐,並且發現居然比希爾排序還要快。微信

 

也許有的讀寫還想追問,有沒有比快速排序更快的方法了呢?沒有了,這個能夠從數學角度證實。若是有人非要說有更快的方式,那他就是在挑戰數學,這和相信永動機的人挑戰熱力學定律沒有本質上的區別。編程語言

 

快速排序,充分利用了分治(divide and conquer)的思想,其核心思想就是少作無用功。ide

 

二. 基本思想

2.1 快速排序的通常過程

快速排序的通常過程是,隨機選取數組中的一個值,做爲比較標準,通常稱之爲樞值(Pivot)。而後把整個數組中小於樞值的數據分到一個組,把大於樞值的數據分到另外一個組,等於樞值的數據分到哪一個組均可以。分紅兩組後,天然不會用其餘排序對小數組進行排序,而是重複以上的步驟,把小的數組再細分。這樣整個數組就由1個數組變成2個數組,再變成4個數組,再變成8個數組,到最後分無可分,簡單比較一下,整個數組就變得有序了。函數

 

簡單描述一下:工具

  1. 選擇樞值。也就是選擇一個數據做爲標杆。選擇樞值實際上是最重要的一個步驟,比較推薦的方法是,選擇數組中第一個、中間以及最後一個數據中的中值。oop

  2. 分組操做。把大於樞值的數據放到樞值右邊,把小於樞值的數據放到左邊。與樞值相等的數據放到哪邊無所謂。學習

  3. 遞歸。對左右兩邊的數據進行樞值選取和分組操做。遞歸的中止條件是細分數組數據個數爲0或者1。

 

來看一張稍微複雜一點的圖,圖中陰影部分的數據就是各個階段的樞值。

From Wikipedia

樞值選取方式和分組操做是快速排序的核心,常見的有兩種方案,即LomutoHoare分組方案。

 

2.2 Lomuto分組方案

最簡單也是最多見的分組方式是Lomuto分組方案,該方案直接選取最後一個元素做爲樞值,該方案最明顯的缺點是,當一個數組已是有序的或者數組全部數字相同,反而會出現最糟糕的排序狀況,即複雜度爲O(n²)。

 

不防看一下一、二、三、四、5這樣一個數組。使用該方案首先選擇5爲樞值,則第一次分組後僅左邊有一、二、三、4這四個數字,而右邊沒有任何數字,第二次選擇4爲樞值,結果仍是同樣,這樣每次分組都只能使一個數字變得有序,效率天然就退化成O(n²)。

 

直接上僞碼:

algorithm quicksort(A, lo, hi) is
   if lo < hi then
       p := partition(A, lo, hi)
       quicksort(A, lo, p - 1)
       quicksort(A, p + 1, hi)

algorithm partition(A, lo, hi) is
   pivot := A[hi]
   i := lo
   for j := lo to hi do
       if A[j] < pivot then
           swap A[i] with A[j]
           i := i + 1
   swap A[i] with A[hi]
   return i

 

2.3 Hoare分組方案

另外一個方法則是Hoare的分組方案,經過必定的方法選擇一個樞值,通常選擇數組中間的值,不妨設數組A首尾元素的下標分別爲lo和hi,則樞值

 

Pivot = A[(lo + hi) / 2]

 

固然,爲了不整數溢出問題,通常寫成

 

Pivot = A[lo + (hi - lo) / 2]

 

關於整數溢出有機會再說。其思想是從數組左右兩端開始,從左端向右側查找第一個大於等於樞值的數據,記錄下標爲i,從右端向左側查找第一個小於等於樞值的數據,記錄下標爲j,而後交換A[i]和A[j]。而後繼續如上操做,直到i大於等於j結束,這樣原來的數組就分紅了兩個數組,左側的均小於樞值,右側均大於等於樞值,而後再對子數組重複如上的操做。

 

以數組四、五、三、二、1爲例:

  1. 選取3爲樞值,找到左端數據4大於樞值,右端數據1小於樞值,交換數據獲得一、五、三、二、4,

  2. 繼續向內掃描數據,發現須要交換5和2,這樣獲得一、二、三、四、5

  3. 繼續對兩個子數組一、5和四、5進行如上操做,發現已經完成排序。

 

老規矩,上僞碼:

algorithm quicksort(A, lo, hi) is
   if lo < hi then
       p := partition(A, lo, hi)
       quicksort(A, lo, p)
       quicksort(A, p + 1, hi)

algorithm partition(A, lo, hi) is
   pivot := A[lo + (hi - lo) / 2]
   i := lo - 1
   j := hi + 1
   loop forever
       do
           i := i + 1
       while A[i] < pivot
       do
           j := j - 1
       while A[j] > pivot
       if i >= j then
           return j
       swap A[i] with A[j]

2.4 其餘分組方案

此外,《算法導論》中還提到隨機化,也就是隨機的選擇樞值,可能你不信,不少排序算法都會用到隨機化這個概念,由於不少時候,隨機化帶來的結果每每意想不到的好。這裏暫不贅述,有機會單獨講一講。Sedgewick推薦一種選取樞值的方法被稱爲三數取中(median-of-three),即從數組第一個數據、正中間數據以及最後一個數據中選取中間值爲樞值。三數取中的升級版本也被稱爲ninther,不妨定義函數median-of-three (Mo3):

 

Mo3(A) = median(A[1], A[n/2], A[n])

 

ninther(a) = median(Mo3(first ⅓ of a), Mo3(middle ⅓ of a), Mo3(final ⅓ of a))

 

即取數組前三分之一找出中值,而後取數組中間三分之一找出中值,再取數組最後三分之一找出中值,最後選擇這三個中值中的中值。

 

三. 複雜度

3.1 時間複雜度

快速排序對於已經有序的數組或者全部數據都相等的數組排序的複雜度是O(n²),這種狀況有多種方法優化,好比,能夠嘗試把數據分紅3組,即大於樞值爲一組,等於樞值爲一組,小於樞值爲一組,其緣由很好理解,這裏就不贅述了。也能夠評估數據的個數,對於較少的數據,徹底不須要使用快速排序,能夠直接使用選擇排序或者希爾排序。

 

快速排序的平均複雜度是O(n*log n),除了快速排序還有歸併排序和堆排序的複雜度也是O(n*log n),那爲何通常都說快速排序是最快的排序算法呢?其實讀過我以前關於複雜度的文章的讀者應該都知道,對於複雜度爲O(C*n*log n)的算法,其複雜度都是O(n*log n),這裏的C爲常數。快速排序之因此最快,主要是由於它的常數C比較小,在具體應用中,快速排序的表現也每每比較好的。

 

3.2 空間複雜度

快速排序的空間複雜度和具體的實現方式有關。Sedgewick描述的一種方案,針對就地(in-place)排序實現,首先經過遞歸對元素最少的分組進行排序,最多須要O(log n)的空間,而後使用尾部遞歸或者迭代的方式對另外一部分分組數據排序,這就避免了把這部分排序操做添加到調用棧,也就是說,這樣能夠限制調用棧的深度不會超過O(log n),也就保證了空間複雜度爲O(log n)。其餘一些非就地(not-in-place)排序實現,空間複雜度則爲O(n)。

 

其實也能夠換個角度理解快速排序的空間複雜度。快速排序的遞歸過程能夠用二叉樹來表示,則調用棧的層次和二叉樹的深度保持一致,那麼最好的狀況樹的深度爲O(log n),即此時空間複雜度爲O(log n)。而最壞的狀況發生在二叉樹退化成單鏈,深度爲O(n),空間複雜度也就是O(n)了。

 

3.3 穩定性和實際表現

快速排序也是不穩定的排序算法。

 

實測一下算法表現,簡單看一下插入排序、希爾排序以及快速排序的效率,以10000個隨機數排序耗時爲例:

 

 

很顯然,快速排序耗時僅有希爾排序的一半。想要獲取源碼,後臺回覆『快速排序』獲取源碼。

 

四. 小結分析

快速排序的總體思想很簡單,就是先按照必定的標準(樞值),先把數據三六九等的分開,而後在小範圍內繼續三六九等的分下去,直到分無可分。也就是每一個數據都找到本身的位置了。快速排序的空間複雜度遠大於希爾排序,這也再次說明了,在計算機科學中,處處都存在用空間換時間的權衡(trade-off)。

 

至於快速排序樞值的選擇,方法有無數種,表現也良莠不齊。可是隻要聽從其核心思想,排序的速度就會有質的提高。從代碼實現角度看,快速排序的實現僅需10多行代碼,可見,好的東西每每是極其簡單的,若是你把一件事搞複雜了,不妨停下腳步,思考一下是否是作事的方法出了問題。

 

不少時候,作事其實也應該如此,要學會分而治之,把大的問題按照必定的標準無限細分下去,到最底層時,只須要作不多的事情就能夠完成一個大目標。就像一個公司,不一樣人分到不一樣的崗位,而後各司其職,就能夠創造一個偉大的公司。

 

微信掃碼或者搜索『icolakele』,後臺回覆『快速排序』獲取源碼。