排序算法——快速排序的圖解、代碼實現以及時間復雜度分析


在C++的泛型排序中,拷貝對象需要很大的開銷,而比較對象常常是相對省時的(編譯器的自動優化)。在這種情況下,如果我們能夠使用更少的數據移動,那么有理由讓一個算法多使用一些比較。而快速排序(Quicksort)滿足了這種特點,實際上C++中通常所使用的排序例程就是使用的快速排序。
快速排序也是一種分治的遞歸算法。它的平均運行時間是O(NlogN),最壞情形性能為O(N2)。

將數組S排序的基本算法由下列簡單的四步組成:

  1. 如果S中元素個數是0或1,則返回
  2. 取S中的任一元素V,稱之為樞紐元(pivot)
  3. 將S-{V}(S中的其他元素)划分成兩個不相交的集合:S1={小於V的元素},S2={大於V的元素}。
  4. 返回{quicksort(S1)后跟V,繼而返回quicksort(S2)}

實現第2步和第3步有很多方法,下面介紹的方法是大量分析和實驗的結果。

一、選取樞紐元

雖然上面說隨機選取一個元素作為樞紐元,但是有些選擇顯然優於其他選擇。

一種錯誤的方法

通常的、無知的選擇是將第一個元素用作樞紐元。如果輸入是隨機的,那么這是可以接受的,而如果輸入是預排序的或是反序的,那么所有的元素不是都被划入S1就是都被划入S2,這將花費二次時間。而且,預排序的輸入(或具有一大段預排序數據)是相當常見的。因此,使用第一個元素作為樞紐元是絕對可怕的壞主意。

一種安全的做法

一種安全的方法是隨機選取樞紐元。一般來說這種策略非常安全,因為隨機的樞紐元不可能總在接連不斷地產生劣質的分割。另一方面,隨機數的生成一般開銷很大,根本減少不了算法其余部分的平均運行時間。

三數中值分割法(Median-of-Three Partitioning)

樞紐元的最好選擇是數組的中值。不幸的是,這很難算出並且會明顯減慢快速排序的速度。一般的做法是使用左端、右端和中心位置上的三個元素的中值作為樞紐元。顯然使用三數中值分割法消除了預排序輸入的壞情形,雅思高分范文並且實際減少了14%的比較次數。

二、分割策略

暫時假設所有的元素互異。一種高效的做法是,將樞紐元與最后一個元素交換使得樞紐元脫離分割,i從第一個元素開始而j從倒數第二個元素開始。
在分割階段要做的就是把所有小元素移到數組的左邊而把所有的大元素移到數組的右邊,當然,小和大是相對於樞紐元而言的。
i右移到大於樞紐元的位置,j左移到小於樞紐元的位置,如果i < j ,那么交換i、j對應的元素,其效果是把一個大元素推向右邊而把小元素推向左邊。以此類推,知道i=j。分割的最后一步是將樞紐元與i所指向的元素交換。

三、圖解演示

以序列8,1,4,9,6,3,5,2,7,0為例,最左邊元素為8,右邊元素是0,中心位置元素是6,於是選定樞紐元pivot=6。
一輪快速排序的圖解步驟如下:
img2
遞歸進行,直至最終有序。
注意,對於很小的數組(N<=20),快速排序不如插入排序。雅思聽力詞匯通常的解決方法是對於小的數組不使用遞歸的快速排序,而使用諸如插入排序這樣的對小數組有效的排序算法。使用這種策略實際上可以節省大約15%的運行時間。

四、代碼實現

完整的Java代碼實現

測試代碼

測試結果

五、快速排序的復雜度分析

正如歸並排序那樣,快速排序也是遞歸的。它的分析需要求解遞推公式(有興趣的可以參考《數據結構與算法分析 Java語言描述 第三版》203頁)。
最壞情況下(例如前面所說,數組倒序,樞紐元選第一位)快速排序將花費O(N2)時間。
最好情況下,快速排序和歸並排序一樣,花費O(NlogN)時間。
平均情況下,快速排序花費O(NlogN)時間。

六、總結

完整項目已經更新到github,訪問地址為:https://github.com/Dodozhou/Algorithm,包路徑為:src/main/java/algorithm/quicksort


免責聲明!

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



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