常見的排序演算法
Asymptotic Notations 與演算法的關係
一個演算法的 time complexity: 執行時間 = f(輸入資料量) ... 這個函數的成長速率越快, 表示對應的演算法執行速率越慢。
為什麼分析演算法的 time complexity 時, 通常寫 O 而不寫 Theta ? 因為估計過程中, 我們秉持著悲觀, 保守, 寧可高估不要低估的原則, 所以估計出來的函數, 是真正所需執行時間的上限。 (實際上可能不需要那麼久)
Selection Sort
最簡單的排序演算法之一: selection sort (選擇排序): 將 n 張考卷中最低分的那一個調到最前面, 再將剩下 (n-1) 張考卷中最低分的那一個調到第二個位置, 再將剩下 (n-2) 張考卷中最低分的那一個 ...
Selection sort 外層迴圈的 loop invariant: 第 k 步做完時, 所有考卷當中最低分的 k 張已就定位。
Selection sort 的 time complexity: n + (n-1) + (n-2) ... + 2 + 1 屬於 O(n^2)", 意思是如果輸入 n 筆資料, 則最多 (最悲觀的情況下) 花 c n^2 的時間必能用 selection sort 演算法完成排序。 (那個 c 是什麼意思? 就是我們不在意的常數倍。)
Insertion Sort
最簡單的排序演算法之二: insertion sort (插入排序): 最前面 2 張考卷的順序是否需要調整? 最前面 3 張考卷的順序是否需要調整? 最前面 4 張考卷的順序是否需要調整? ...
Insertion sort 外層迴圈的 loop invariant: 第 k 步做完時, 最前面 k 張考卷彼此之間的相關順序正確。 (但將來有可能被其他尚未處理的考卷擠到後面去)
一個排序演算法在完成後, 若能夠保留所有 "值相同" 的資料之間原來的相對順序, 則稱它為 stable。 例如標準版的 selection sort 是 non-stable; 而 insertion sort 容易寫成 stable。 (Q: 寫程式時, 那裡要注意?)
Insertion sort 的 time complexity: 1+2+3...+n 屬於 O(n^2); 但運氣好的話, 最快只需要 O(n)。 Q: 怎麼樣的情況會造成執行時間只需要 O(n)?
請見 selection sort 與 insertion sort 的範例
Mergesort
先前 看過的 mergesort 是一種 divide-and-conquer 類型的演算法 -- 把一個大問題切成幾個小一號的問題, 個個擊破。 如果這些小一號的問題, 與原問題長得很像, 就可以寫 遞迴 recursive 程式來解。
摘要: 「把資料等分成兩堆, 各自用 mergesort 排序好後再合併。」 其中最主要的工作都在 "合併" 動作當中完成。
Mergesort 有兩種: top-down 及 bottom-up。 請見 範例。
分析 mergesort 的 time complexity: 只考慮 n = 2^k 這種狀況, 因為這種狀況比較容易算, 更重要的是因為只考慮這種狀況就夠了。 (記得嗎? "不在意零頭, 不在意常數倍; 在意的是趨勢, 是成長的速度.") 耗時 T(n) = 2*T(n/2) + n
一個排序演算法, 若只需要用到 O(1) 的額外空間, 則稱它具有 in-place 特性。 例如 insertion sort, selection sort 都是 in-place。 但 merge sort 就不是 in-place, 它在 merge 時耗費 O(n) 的空間。
Mergesort 可以是 stable (Q: 寫程式時, 那裡要注意?)。
Quicksort
Quicksort 也是一種 divide-and-conquer 類型的演算法, 也可以用遞迴來實作。
摘要: 「挑一個元素當做 pivot, 把陣列內比它小元素的都放在同一側;
把比它大的都放在另一側。 然後對兩側使用 quicksort。」
請見 範例程式。 可以修改 @a = qw(...)
那一句, 換不同的資料來測試看看。
如何分側? 「從左右向中間掃描, 每找到一對放錯側的元素, 就把它們對調過來。 對調後繼續向中間掃描, 直到兩個指標交錯為止.」 這個分側動作叫做 partition 注意: 完成 partition 動作後, 這個 pivot 元素自然也就落到它最終的正確位置。
簡單版的 quicksort 以陣列的最左邊元素作為 pivot。
執行方式: ./qsort -g -p left
Time complexity:
- best case: O(n lg n), 因為 T(n) = 2*T(n/2) + n
- worst case: O(n^2), 因為 T(n) = T(n-1) + n
- average case: O(n lg n) (即使是 T(n) = T(9n/10) + T(n/10) + n 這麼不理想的情況, 還是 O(n lg n), 所以 ...)
Quicksort 不適合用於「幾乎已排好」或「幾乎正好排顛倒」的資料。
改進一: randomized quicksort: 用亂數決定要選取那一個元素當做
pivot, 而不是固定找第一個元素。 不論是 best/worst/average case 的
time complexity 都沒有改變; 但是如此可以避免上述狀況經常發生。
請執行 ./qsort -g -p rand
看看
改進二: median-of-3 quicksort: 挑陣列的第一個, 中間的,
及最後一個元素, 用這三個之中的中位數 (median) 來當做 pivot。
除非這三個數字當中正好有兩個非常小, 或有兩個非常大 (機率很小),
否則所選出來的 pivot 應該可以讓 partition 把陣列分得比較平均些。
請執行 ./qsort -g -p mo3
看看
可以把以上兩種改進合併, 先用亂數任選三個, 再用其中的 median 當做 pivot。
改進三: 運用 insertion sort 的特性 (若資料原本就幾近排序完成, 則 insertion sort 只需花 O(n) 的時間), 修改 quicksort: 遞迴到陣列內少於 k 個元素時, 就不要遞迴下去了。 每一小段 k 個元素都放著不排序, 等最上層 quicksort 完成後, 再一次呼叫 insertion sort 來收拾殘局。
Q: Quicksort 是否為 stable? Q: Quicksort 是否為 in-place? 表面上好像是; 其實因為它遞迴, 所以用了 stack 空間。 Q: Quicksort 最壞的情況下會用到 O(n) 的 stack 空間。 什麼樣的情況? Q: 如何改寫 quicksort, 讓它使用的 stack 空間不超過 O(lg n)? Q: Mergesort 也是遞迴, 為什麼我們先前沒有討論使用 stack 空間的問題?
題外話: 寫程式時應善用系統既有的資源。 許多程式語言的函式庫當中都已有現成的 quicksort, 應多加利用。 例如 C 當中的 qsort 即是。 但要學會使用 指到函數的指標變數。
如果你一定要自己寫 quicksort 程式, 這裡有一些細節及心得與你分享:
- 兩頭需要有 sentinel 來阻止兩指標跑過頭。 如果你以最左或最右元素為 pivot, 就可以省掉一個 sentinel。 在排中間段落的時候, 兩邊的資料自動成為 sentinel。 如果你用 median-of-3, 那麼兩頭的 sentinel 都可以省略掉。
- 左右指標在跑的時候, 為什麼遇到與 pivot 等值的元素也要停下來? 這樣 pivot 本身就可以作為 sentinel。
- 每次左右兩指標交錯, partition 動作結束後, 你的左 pivot 應與右指標的元素對調 (因為當時它已指向較小元素); 反之, 如果當初你以右元素為 pivot, 那麼此時應將 pivot 與左指標的元素對調。
- 若要選其他元素作為 pivot, 請先將它與左 (右) 元素對調, 程式比較好寫。
Mergesort 與 Quicksort 其實很像
Mergesort 與 Quicksort 的比較: 都是 Divide-and-Conquer 的演算法, 只是 quicksort 先苦後樂 (進入遞迴之前的 partition 動作比較麻煩; 遞迴結束之後什麼都不必再做) 而 mergesort 先樂後苦 (進入遞迴之前什麼都不必做; 遞迴結束之後的 merge 動作比較麻煩)。
void quicksort(...) void mergesort(...) { { partition(...); quicksort(...); mergesort(...); quicksort(...); mergesort(...); merge(...); } }
- 本頁最新版網址: https://frdm.cyut.edu.tw/~ckhung/b/al/sort1.php; 您所看到的版本: February 14 2012 10:32:24.
- 作者: 朝陽科技大學 資訊管理系 洪朝貴
- 寶貝你我的地球, 請 減少列印, 多用背面, 丟棄時做垃圾分類。
- 本文件以 Creative Commons Attribution-ShareAlike License 或以 Free Document License 方式公開授權大眾自由複製/修改/散佈。