視頻動畫 | 什麼是快速排序?

點擊藍色「五分鐘學算法」關注我喲

加個「星標」,天天中午 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 小時一起學算法