快速排序演算法到底有多快?
速排序演算法是最流行的排序演算法,因為有充足的理由,在大多數情況下,快速排序都是最快的,執行時間為 O(NlogN) 級(這只是對內部排序或者說隨機儲存器內的排序而言,對於在磁碟檔案中的資料進行的排序,其他的排序演算法可能更好)。快速排序本質上通過一個數組劃分為兩個子陣列,然後遞迴地呼叫自身為每一個子陣列進行快速排序來實現的,即演算法分為三步:
-
1 把陣列或者子陣列劃分為左邊(較小的關鍵字)的一組和右邊(較大的關鍵字)的一組;
-
2 呼叫自身對左邊的一組進行排序;
-
3 呼叫自身對右邊的一組進行排序。
經過一次劃分之後,所有在左邊子陣列的資料項都小於在右邊子陣列的資料項,只要對左邊子陣列和右邊子陣列分別進行排序,整個陣列就是有序的了。下面試一次劃分後的示意圖:

快速排序需要劃分陣列,這就用到了劃分演算法。劃分演算法是由兩個指標(這裡是指陣列資料項,非 C++ 中所說的指標)開始工作,兩個指標分別指向陣列的兩頭,左邊的指標 leftPtr 向右移動,右邊的指標 rightPtr 向左移動。當 leftPtr 遇到比樞紐(待比較的資料項,比其小的在其左邊,比其大的在其右邊,下面均稱之為“樞紐”)小的資料項時繼續右移,當遇到比樞紐大的資料項時就停下來;類似的 rightPtr 想反。兩邊都停下後,leftPtr 和 rightPtr 都指在陣列的錯誤一方的位置的資料項,交換這兩個資料項。交換後繼續移動這兩個指標。
基於上面的劃分演算法,可以將資料快速排好序,下面是快速排序的實現程式碼:
演算法分析:快速排序是一種不穩定的排序方法,其平均時間複雜度為 O(NlogN),最壞的情況下退化成插入排序了,為 O(N^2)。
快速排序是不穩定的,當 a=b>pivot 且 a 在 b 前面的時候,由於從後面開始遍歷,故 b 會先於 a 被替換到 pivot 的前面,這樣,b 就變成了在 a 的前面,也就是說,ab 位置對調,故該排序演算法不穩定。
空間複雜度平均為 O(logN),空間複雜度主要是由於遞迴造成的。
在理想狀態下應該選擇被排序的資料項的中值資料項作為樞紐(上面程式中是用陣列的最後一項作為樞紐的)。也就是說,應該有一半的資料項大於樞紐,一般的資料項小於樞紐。這會使陣列被劃分成兩個大小相等的子陣列。對於快速排序演算法來說,擁有兩個大小相等的子陣列是最優的情況,最壞的情況就是一個子陣列只有一個數據項,另一個子陣列含有N-1個數據項。所以上面的演算法中如果最右邊的資料是最小的或者最大的,那就可能導致最壞的情況出現。為了解決這個問題,我們可以改進上面的演算法,使用“三資料項取中”劃分:找到數組裡的第一個、最後一個以及中間位置資料項的值,將三個中處在中間大小的資料項作為樞紐,且將三個數排好序。下面是改進的快速排序:
演算法分析:三資料項取中法除了對選擇樞紐更為有效外,還有另一個好處:可以對第二個內部 while 迴圈中取消 rightPtr>left(即 rightPtr>0)的測試,以略微提高演算法的執行速度。因為在選擇的過程中使用三資料項取中法不僅選擇了樞紐,而且對這三個資料項進行了排序,所以就可以保證陣列最左端的資料項小於或者等於樞紐,最右端的資料項大於或者等於樞紐,所以就可以省去 rightPtr<0 的檢測了,leftPtr 和 rightPtr 不會分別越過陣列的最右端或者最左端。
三資料項取中還有一個小的好處是,對左端、中間以及右端的資料項排序後,劃分過程就不需要再考慮這三個資料項了,所以上面的程式中左端真正是從 left+1 處開始的,右端真正是從 right-2 處開始的(因為 right 處存的是比樞紐大的資料項,right-1 處存的是樞紐)。
如果使用三資料項取中劃分方法,則必須要遵循快速排序演算法不能執行三個或者少於三個項的劃分規則。在這種情況下,數字3被稱為切割點(cutoff)。在上面的例子中,我們用一段程式碼手動對兩個或三個資料項的子陣列來排序的,但是這不是最好的方法。
處理小劃分的另一個選擇是使用插入排序。當使用插入排序的時候,不以限制3為切割點,可以把界限定位10、20或者其他任何數,試驗不同切割點的值找到最好的執行效率是很有意義的。最好的選擇值取決於計算機、作業系統、編譯器等。這裡使用9作為切割點。也就是說,當待比較的數小於等於9時,我們使用插入排序,大於9時我們使用快速排序法。繼續修改上面的程式:
經過兩次改進後,這樣快速排序便結合了插入排序,三資料項取中法等方法,算是比較好的一個演算法了。
原文釋出時間為:2018-10-24
本文作者:Java技術驛站
本文來自雲棲社群合作伙伴“ ofollow,noindex">Java技術驛站 ”,瞭解相關資訊可以關注“ Java技術驛站 ”。