點擊藍色「五分鐘學算法」關注我喲
加個「星標」,天天中午 12:15,一起學算法
快速排序屬性
上一篇文章介紹了 冒泡排序和它的優化 。這次介紹的快速排序是冒泡排序演變而來的算法,比冒泡排序要高效的很多。
快速排序之所以快,是因爲它使用了分治法。它雖然也是通過不斷的比較和移動來實現排序的,只不過它的實現,增大了比較的距離和移動的距離。而冒泡排序只是相鄰的比較和交換。
快速排序的思想是,通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。
從字面上感覺不到它的好處,我們通過一個示例來理解基本的快速排序算法,假設當前數組元素是:5, 1, 9, 3, 7, 4, 8, 6, 2。
基本的快速排序算法
初始狀態:5, 1, 9, 3, 7, 4, 8, 6, 2
選擇5作爲一個基準元素,然後從右向左移動hight下標,進行基準元素和下標爲hight的元素進行比較。
如果基準元素要大,則進行hight的左移一位;如果基準元素要小,則進行元素的交換。
在hight下標左移的過程中,我們目的是找出比基準元素小的,然後進行交換。
交換完之後,進行left的右移,找出比基準元素大的,找到則進行交換。
視頻動畫
Code
Result
發生交換 [2, 1, 9, 3, 7, 4, 8, 6, 5]
發生交換 [2, 1, 5, 3, 7, 4, 8, 6, 9]
發生交換 [2, 1, 4, 3, 7, 5, 8, 6, 9]
發生交換 [2, 1, 4, 3, 5, 7, 8, 6, 9]
算出樞軸值 5 和對應下標 4
發生交換 [1, 2, 4, 3, 5, 7, 8, 6, 9]
算出樞軸值 2 和對應下標 1
發生交換 [1, 2, 3, 4, 5, 7, 8, 6, 9]
算出樞軸值 4 和對應下標 3
發生交換 [1, 2, 3, 4, 5, 6, 8, 7, 9]
發生交換 [1, 2, 3, 4, 5, 6, 7, 8, 9]
算出樞軸值 7 和對應下標 6
算出樞軸值 8 和對應下標 7
優化樞軸的選取
舉一個恰當的例子,假設數組元素是9,8,7,6,5,4,3,2,1。
進行hight左移的時候第一個就發生了交換,1,8,7,6,5,4,3,2,9。嗯看似效率蠻快,但是進行low右移的時候一個個做了不必要的計算,沒有一個元素比樞軸值要大。和冒泡排序一樣,這一趟進行了8次比較,時間複雜度達到最壞程度O(n^2)。和快排的O(nlongn)相悖。
那拿什麼更好的方式選取樞軸值呢?
我看到網上都說是,隨機選取一個數作爲基準元素。嗯看似一個好的方法,但是和上面大概率出現的最壞情況還是有可能發生的。每次選取樞軸值都有可能是最大的或者最小的。如果是龐大的數據量第一個隨機選到了最大的數,程序卡的半死不活的,只有kill掉再重新運行嗎?
改進情況,取三數之中的中間數的一個數。什麼意思呢?
就是在一組數中取三個關鍵數字,將中間數作爲樞軸,一般可以取左端,右端和中間三個數,也可以隨機選取。那你可能說了,要是三個數都是最小的或者都是最大的那什麼辦呢?
沒錯,這樣選取還是會帶來時間上的開銷,並不證明選取到一個好的樞軸值。那要是取九數之中的中間數呢?
這當然不是採用隨機取九個數然後再排序排一半取中間數那一個。
它是從數組中分三次取樣,每次取三個數,三個樣品中各取出中間數,然後在這三個中樞當中再取一箇中間數作爲樞軸。如果一次極端就算了,但是分三次取樣還會碰到三次極端那顯然是微乎其微的。這樣的方法增加選到好的樞軸的概率。
優化不必要的交換
回到基本的快速排序算法,回顧上面的視頻動畫。我們可以發現,這其中發生了不必要的移動方式。
我們最終要求一趟選的樞軸值,大的數在它的右邊,小的數在它左邊。但是這個樞軸值每次符合條件去了不該去的地方。我認爲它前面的地方不要動,等一趟完了就去自己該去的地方,減少時間上的消耗。
視頻動畫
Code
Result
發生交換 [5, 1, 2, 3, 7, 4, 8, 6, 9]
發生交換 [5, 1, 2, 3, 4, 7, 8, 6, 9]
發生交換 [4, 1, 2, 3, 5, 7, 8, 6, 9]
算出樞軸值 5 和對應下標 4
發生交換 [3, 1, 2, 4, 5, 7, 8, 6, 9]
算出樞軸值 4 和對應下標 3
發生交換 [2, 1, 3, 4, 5, 7, 8, 6, 9]
算出樞軸值 3 和對應下標 2
發生交換 [1, 2, 3, 4, 5, 7, 8, 6, 9]
算出樞軸值 2 和對應下標 1
發生交換 [1, 2, 3, 4, 5, 7, 6, 8, 9]
發生交換 [1, 2, 3, 4, 5, 6, 7, 8, 9]
算出樞軸值 7 和對應下標 6
算出樞軸值 8 和對應下標 7
優化遞歸操作
我們都知道,遞歸對性能是有一定影響的,quickSort函數尾部有兩次遞歸操作。如果待排序的序列極爲極端不平衡,遞歸的深度幾乎接近於n的高度(沒有了二分法的優勢)。這樣的時間複雜度也是達到了最壞的程度O(n^2),而不是平衡時的O(nlogn)。
時間慢也就算了,但是棧的大小也是有限的,每次遞歸操作都消耗一定的棧空間,函數的參數越多,每次遞歸調用參數耗費的空間也是越多。
如果能減少遞歸,性能也因此大大提高。
那拿什麼方式優化遞歸操作呢?
來看下面代碼。
Code
Result
發生交換 [2, 1, 9, 3, 7, 4, 8, 6, 5]
發生交換 [2, 1, 5, 3, 7, 4, 8, 6, 9]
發生交換 [2, 1, 4, 3, 7, 5, 8, 6, 9]
發生交換 [2, 1, 4, 3, 5, 7, 8, 6, 9]
算出樞軸值 5 和對應下標 4
發生交換 [1, 2, 4, 3, 5, 7, 8, 6, 9]
算出樞軸值 2 和對應下標 1
發生交換 [1, 2, 3, 4, 5, 7, 8, 6, 9]
算出樞軸值 4 和對應下標 3
發生交換 [1, 2, 3, 4, 5, 6, 8, 7, 9]
發生交換 [1, 2, 3, 4, 5, 6, 7, 8, 9]
算出樞軸值 7 和對應下標 6
算出樞軸值 8 和對應下標 7
執行結果之後和前面兩個結果無異。這是一個很好的方法。我們把if改成while,然後一次遞歸之後,low已經沒有用處了,所以把pivot+1賦值給low作爲下一個參數。結果你也看到了,結果都相同。
因此採用迭代而不是遞歸的方法可以縮減堆棧深度,從而提高了整體性能。
-----------------------
公衆號:五分鐘學算法(ID:CXYxiaowu)
博客:www.cxyxiaowu.com
知乎:程序員吳師兄
一個正在學習算法的人,致力於將算法講清楚!
長按下圖二維碼關注,和你一起領悟算法的魅力。
戳一下下方的小程序,24 小時一起學算法