【Python進階教學】進階排序法:快速排序法 / 合併排序法【part 17】 === 目錄(Table of Contents) [TOC] --- 哈囉大家好,很感謝你點進本文章,我是一名高中生,是電腦社社團的社長,由於我並不是 Python 這方面非常精通的專家,所以若文章有些錯誤請見諒,也可向我指正錯誤。另外本文章的用意是作為電腦社社團的教材使用而編寫的。 那麼,讓我們開始吧。 快速排序法(Quick Sort) === ![image](https://hackmd.io/_uploads/Sy-WLMVHC.png) (快速排序法概念圖)Image Source:[Unraveling QuickSort: The Fast and Versatile Sorting Algorithm | by Nathal Dawson | Medium](https://medium.com/@nathaldawson/unraveling-quicksort-the-fast-and-versatile-sorting-algorithm-2c1214755ce9) 快速排序法步驟: 1. 序列中尋找一個基準值(pivot) 2. 進行快速排序法,所有比基準值小的排左邊,比他大的排右邊。 3. 使用遞迴式對左右兩邊的子序列作與 2. 相同排序。 註:基準值可使用隨機的抽取方式,或是自定義一個基準值。 看到這邊,或許你發現到快速排序法其實類似於二分法的步驟,只不過特別的是,快速排序法需要使用到遞迴式,這個概念使用到了所謂的 ***分治法(divide and conquer)*** 的其中一種。 讓我們舉個具體的例子,以上面的概念圖為例: 首先概念圖取 3 作為快速排序法中的基準值, 所以,比 3 小的排左邊,像是 -3、2、-6、1;比 3 大的排右邊,像是 8、5、9、6。 左右兩邊分別再次取基準值,跟前面步驟做一樣的事情: * 第一次左分支: 取最右邊 1 作為基準值,進行排序,同樣比它小排左邊,比它大排右邊。左:-3、-6;右:2(只剩一個就不須再分) * 第一次右分支: 取最右邊 6 作為基準值,進行排序,同樣比它小排左邊,比它大排右邊。左:5(只剩一個就不須再分);右:9、8 * 第二次左分支: 取 -6 作為基準值,分為 -6、-3,不能再分了,結束。 * 第二次右分支: 取 8 作為基準值,分為 8、9,不能再分了,結束。 那最後我們的程式會自動將這排序好的結果(只看單一方塊的那個數字就是排序好的)遞迴回去,像是 -6、-3;8、9 是已經排序好的,把它遞迴回去,最後排序完成:-6、-3、1、2、3、5、6、8、9。 以下是快速排序法的重點: 快速排序法中,選擇基準值是非常重要的一件事情,如果沒選對基準值的話,這個方法的時間複雜度為 O(n^2)。如果選對的話,時間複雜度則會降為 O(n log n)。 Python 實作 --- 以下範例程式碼參考自書籍:演算法:圖解邏輯思維 + Python程式實作王者歸來 ```python= import random def quick_sort(nLst): if len(nLst) <= 1: return nLst left = [] right = [] piv = [] pivot = random.choice(nLst) for val in nLst: if val == pivot: piv.append(val) elif val < pivot: left.append(val) else: right.append(val) return quick_sort(left) + piv + quick_sort(right) data = [9, -3, 5, 2, 6, 8, -6, 1, 3] print("原始列表 :", data) print("排序結果 :", quick_sort(data)) ``` 程式碼解釋: * line 1 : 引入 random 隨機亂數模組 * line 3 ~ 4 : 判斷 nLst 長度是否只有 1,是的話就回傳 nLst 列表本身。 * line 6 ~ 8 : 初始化局域變數 left, right, piv,這邊其實可使用多變數賦值方式:left = right = piv = [] * line 9 : 使用 random.choice(seq) 方法,seq 為一個序列,功能為在序列當中隨機取一項元素,再回傳那個元素。 * line 10 : 遍歷 nLst 列表。 * line 11~12 : 判斷 val 如果等同於 pivot(基準值),則將 val 塞到 piv 列表的最末尾。 * line 13~14 : 判斷 val 是否小於 pivot,是的話就將 val 塞到 left 列表的最末尾(小的在左邊)。 * line 15~16 : 由於上述條件不成立,最後肯定判斷 val 是大於 pivot 的,則將 val 塞入 right 列表的最末尾(大的往右邊)。 * line 17 : 回傳 quick_sort(left) + piv + quick_sort(right),進行遞迴式,列表相加的意義為列表之間互相連接,彷彿兩個斷裂的橋樑再次接合一樣。 小結 --- 快速排序法步驟: 1. 序列中尋找一個基準值(pivot) 2. 進行快速排序法,所有比基準值小的排左邊,比他大的排右邊。 3. 使用遞迴式對左右兩邊的子序列作與 2. 相同排序。 時間複雜度: * 選對基準值:O(n log n) -> 效率佳 * 選錯基準值:O(n^2) -> 效率差 合併排序法(Merge Sort) === ![MergeSort_Avg_case](https://hackmd.io/_uploads/HJFc17ESR.gif) (合併排序法 gif 概念動圖)Image Source:[Merge Sort | Sorting Algorithm | Code Pumpkin](https://codepumpkin.com/merge-sort-sorting-algorithm/) 合併排序法演算法的精神與快速排序法是相同的,同屬於分治法(divide and conquer)。 合併排序法主要是先將序列進行 ***分割(divide)*** 成近乎等長的兩序列,重複處理至序列只剩下一個元素,直到無法再被分割,這點與快速排序法是相同的。 分割完之後呢,接下來進行 ***合併(conquer)*** 被分割的序列,主要將已排序好的最小單位之數列開始合併,重複處理到合併與原序列相同長度的序列。 再整理一下: 合併排序法主要動作為 ***分割(divide)、合併(conquer)***。 * 分割(divide):把序列平分(//2,只取整數),接著繼續將各個平分後的序列繼續切,切到不能再切為止(剩下 1 個元素)。 * 合併(conquer):把這些已排序最小單位的序列(就是那個切到不能再切的序列)合併,重複合併到與原序列相同大小。 接下來讓我們演示一下分割的步驟,如下圖: ![圖一](https://hackmd.io/_uploads/S1XuCUVrR.png) 接著繼續切,如下圖: ![圖二](https://hackmd.io/_uploads/B16ORLEB0.png) 還是繼續切,切到個數與原序列的個數相同。 ![圖三](https://hackmd.io/_uploads/r1HKC8NBA.png) ![圖四](https://hackmd.io/_uploads/BJ6Y0INHC.png) 切到如上圖這樣,分割基本上就完成了。 接下來合併起來: ![圖五](https://hackmd.io/_uploads/rJGpDwVr0.png) 等下你就會發現,這個排序法會在合併的時候進行排序。 ![圖六](https://hackmd.io/_uploads/rkc0ww4SR.png) ![圖七](https://hackmd.io/_uploads/By0CPwVB0.png) ![圖八](https://hackmd.io/_uploads/r1zJ_D4SC.png) ![圖九](https://hackmd.io/_uploads/By2J_DVHA.png) ![圖十](https://hackmd.io/_uploads/BkfldP4BR.png) 所以合併是進行排序的過程。 合併排序法的時間複雜度是 O(n log n)。因為資料有 n 個,所以可以建立 log n 的階層樹,無論怎麼去建立,都必須處理 n 個數,所以時間複雜度才是 O(n log n)。 Python 實作 --- 以下範例程式碼參考自書籍:演算法:圖解邏輯思維 + Python程式實作王者歸來 ```python= def merge(left, right): ''' 兩數列合併 ''' output = [] while left and right: if left[0] <= right[0]: output.append(left.pop(0)) else: output.append(right.pop(0)) if left: output += left if right: output += right return output def merge_sort(nLst): ''' 合併排序 ''' if len(nLst) <= 1: return nLst mid = len(nLst) // 2 left = nLst[:mid] right = nLst[mid:] left = merge_sort(left) right = merge_sort(right) return merge(left, right) data = [4, 1, 7, 5, 3, 2, 6] print("原始串列 :", data) print("排序結果 :", merge_sort(data)) ``` 程式碼解釋: * Line 3 : 初始化 output 變數。 * Line 4 : while 迴圈,判斷 left、right 存在,則執行迴圈。 * Line 5 ~ 6 : 判斷 left[0] <= right[0],成立則將 left.pop(0) 取出 left 第一個值後回傳的值放入 output 當中。 * Line 7 ~ 8 : 否則將 right.pop(0) 取出 right 第一個值後回傳的值放入 output 當中。 * Line 9 ~ 10 : 如果 left 存在,output 與 left 列表連結。 * Line 11 ~ 12 : 如上。 * Line 13 : 回傳 output 列表值。 * Line 19 : 定義 mid 變數為 len(nLst) // 2,求取列表長度除於 2 的值,只取整數。 * Line 20 : left = nLst[:mid] 擷取列表值,從 0 開始直到 (mid - 1) 索引。 * Line 21 : right = nLst[mid:] 擷取列表值,從 mid 索引開始直到末尾的索引。 * Line 22 ~ 23 : 遞迴,進行分割動作。 * Line 24 : 回傳 merge(left, right) 合併排序後的結果。 小結 --- 合併排序法主要動作為 ***分割(divide)、合併(conquer)***。 * 分割(divide):把序列平分(//2,只取整數),接著繼續將各個平分後的序列繼續切,切到不能再切為止(剩下 1 個元素)。 * 合併(conquer):把這些已排序最小單位的序列(就是那個切到不能再切的序列)合併,重複合併到與原序列相同大小。 時間複雜度:O(n log n) 總結 === 在排序上通常都預設升序排序,也就是由小到大,所以一定都要有個概念:小的排左邊,大的排右邊。 | 屬性 | 快速排序法(Quick Sort) | 合併排序法(Merge Sort) | | -------- | -------- | -------- | | 基本概念 | 基於選擇基準值(pivot)將數列分為小於基準值和大於基準值的子序列,然後遞迴排序。 | 將數列分割成兩半,遞迴排序各半部,然後合併已排序的子序列。 | | 排序策略 | 分治法:分割、排序、合併 | 分治法:分割、合併 | | 時間複雜度 | 最壞情況 O(n²);平均情況 O(n log n) | O(n log n) | | 空間複雜度 | O(log n)(在原地排序,不需要額外空間) | O(n)(需要額外空間來儲存中間結果) | | 穩定性 | 不穩定(相同值的順序可能會改變) | 穩定(相同值的順序不變) | | 適用情況 | 當空間有限或不需要穩定排序時效果較好 | 當需要穩定排序或有大量數據時效果較好 | | 基準值選擇 | 隨機選擇或選擇特定位置的元素作為基準值 | X | | 分割過程 | 依據基準值分為左右兩部分,左側小於基準值,右側大於基準值 | 將數列平分成兩半,直到每個部分僅含一個元素 | | 合併過程 | 合併步驟在遞迴回程中自然完成 | 將已排序的兩個子序列合併成一個完整的排序序列 | 參考資料 === 書籍:演算法:圖解邏輯思維 + Python程式實作王者歸來 [初學者學演算法|排序法進階:合併排序法. 程式麻瓜的程式知識課(六) | by Cheng-Wei Hu | 胡程維 | AppWorks School | Medium](https://medium.com/appworks-school/%E5%88%9D%E5%AD%B8%E8%80%85%E5%AD%B8%E6%BC%94%E7%AE%97%E6%B3%95-%E6%8E%92%E5%BA%8F%E6%B3%95%E9%80%B2%E9%9A%8E-%E5%90%88%E4%BD%B5%E6%8E%92%E5%BA%8F%E6%B3%95-6252651c6f7e) [【Day25】[演算法]-合併排序法Merge Sort - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天](https://ithelp.ithome.com.tw/articles/10278179) [Merge Sort | Sorting Algorithm | Code Pumpkin](https://codepumpkin.com/merge-sort-sorting-algorithm/) [Unraveling QuickSort: The Fast and Versatile Sorting Algorithm | by Nathal Dawson | Medium](https://medium.com/@nathaldawson/unraveling-quicksort-the-fast-and-versatile-sorting-algorithm-2c1214755ce9) [[演算法] 快速排序法 (Quick Sort) - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天](https://ithelp.ithome.com.tw/articles/10202330) [Python choice() 函数 | 菜鸟教程](https://www.runoob.com/python/func-number-choice.html)