排序 泡泡排序 選擇排序 插入排序 合併排序 快速排序

[Algorithm] 排序演算法

演算法概念

排序就是把一堆資料照大小排好,好處是之後查詢、去重、找中位數都便宜。評估一個排序演算法要看四件事:時間(多快)、空間(要不要多開陣列)、穩定性(相等元素順序會不會被打亂)、適應性(資料已經接近有序時會不會更快)。

比較型排序(靠比誰大誰小決定順序)的理論下界是 O(nlogn)O(n \log n),因為 n!n! 種排列要用二元判斷樹區分,至少要 log2n!=Θ(nlogn)\log_2 n! = \Theta(n \log n) 次比較。想突破這個下界只能用非比較型(counting/radix/bucket),但那些有額外假設。

穩定性的例子:[('Alice', 90), ('Bob', 80), ('Charlie', 90)] 依分數升序,Alice 和 Charlie 同分,穩定排序後 Alice 仍在 Charlie 前面。

各適用情境

泡泡排序(Bubble Sort)

演算法策略

相鄰兩個比一比,大的往右推。一輪下來最大的值就冒到最右定位。每輪少掃一個(右邊已定位的不用再比),直到某一輪沒發生任何交換就提早結束。

變數:ii 是外層輪次(代表右邊已有 ii 個定位),jj 是內層指標從 0 掃到 n1in - 1 - i

複雜度

最壞每輪掃 n1,n2,,1n - 1, n - 2, \ldots, 1 次 → n(n1)2=O(n2)\frac{n(n-1)}{2} = O(n^2)。最佳情況(已排序)一輪就發現沒 swap、提早結束 → O(n)O(n)。就地、穩定。

範例

題目[4, 1, 3, 1, 5, 2] 由小到大排序。

圖解

步驟

第 1 輪(i=0i = 0jj 掃 0 到 4。

jjarr[j]arr[j+1]動作陣列
041swap[1, 4, 3, 1, 5, 2]
143swap[1, 3, 4, 1, 5, 2]
241swap[1, 3, 1, 4, 5, 2]
345[1, 3, 1, 4, 5, 2]
452swap[1, 3, 1, 4, 2, 5]

5 冒到最右。

第 2 輪(i=1i = 1jj 掃 0 到 3。

jjarr[j]arr[j+1]動作陣列
013[1, 3, 1, 4, 2, 5]
131swap[1, 1, 3, 4, 2, 5]
234[1, 1, 3, 4, 2, 5]
342swap[1, 1, 3, 2, 4, 5]

第 3 輪(i=2i = 2jj 掃 0 到 2。

jjarr[j]arr[j+1]動作陣列
011[1, 1, 3, 2, 4, 5]
113[1, 1, 3, 2, 4, 5]
232swap[1, 1, 2, 3, 4, 5]

第 4 輪(i=3i = 3:全程無 swap → 提早結束。最終 [1, 1, 2, 3, 4, 5]

Python 實作
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        swapped = False
        for j in range(n - 1 - i):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        if not swapped:
            break
例題:判斷跑幾輪就已排序完成

在 Bubble Sort 中,一輪內若沒發生任何 swap,就代表已經排序完成。所以「跑幾輪剛好排好」= 第一次出現「整輪無 swap」的輪次。

例如 [1, 2, 3, 5, 4]:第 1 輪只在 j=3j = 3 交換 5, 4 → [1, 2, 3, 4, 5],但本輪有 swap。第 2 輪全程無 swap、提早結束。所以跑 2 輪。

BestAverageWorst空間穩定
Bubble SortO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)

選擇排序(Selection Sort)

演算法策略

每輪從未排序區找最小值,跟未排序區的第一個位置交換。左邊已排序區每輪長一格,右邊縮一格。

變數:ii 是外層輪次(也是未排序區起點)、jji+1i + 1 掃到末端、min_idx 記當輪最小值位置。

複雜度

無論輸入如何,外層 n1n - 1 輪、內層每輪平均掃 n/2n/2 次 → 一律 O(n2)O(n^2)。好處是交換次數最少(每輪一次),壞處是不穩定——[3a, 3b, 1] 第 1 輪把 3a3a 換到位置 3,就跳到 3b3b 後面了。

範例

題目[4, 1, 3, 1, 5, 2] 由小到大排序。

圖解

步驟

ii未排序區最小值位置交換交換後陣列
0全部11a[0]a[1]a[0] \leftrightarrow a[1][1, 4, 3, 1, 5, 2]
1[4,3,1,5,2]13a[1]a[3]a[1] \leftrightarrow a[3][1, 1, 3, 4, 5, 2]
2[3,4,5,2]25a[2]a[5]a[2] \leftrightarrow a[5][1, 1, 2, 4, 5, 3]
3[4,5,3]35a[3]a[5]a[3] \leftrightarrow a[5][1, 1, 2, 3, 5, 4]
4[5,4]45a[4]a[5]a[4] \leftrightarrow a[5][1, 1, 2, 3, 4, 5]
Python 實作
def selection_sort(arr):
    n = len(arr)
    for i in range(n):
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
例題:13 個元素跑前 5 輪

陣列 a[1..13]=[12,30,8,2,16,28,4,10,1,13,20,6,18]a[1..13] = [12, 30, 8, 2, 16, 28, 4, 10, 1, 13, 20, 6, 18],用 1-indexed,跑前 5 輪。

ii未排序區最小值位置交換陣列
1a[1..13]a[1..13]19a[1]a[9]a[1] \leftrightarrow a[9]1, 30, 8, 2, 16, 28, 4, 10, 12, 13, 20, 6, 18
2a[2..13]a[2..13]24a[2]a[4]a[2] \leftrightarrow a[4]1, 2, 8, 30, 16, 28, 4, 10, 12, 13, 20, 6, 18
3a[3..13]a[3..13]47a[3]a[7]a[3] \leftrightarrow a[7]1, 2, 4, 30, 16, 28, 8, 10, 12, 13, 20, 6, 18
4a[4..13]a[4..13]612a[4]a[12]a[4] \leftrightarrow a[12]1, 2, 4, 6, 16, 28, 8, 10, 12, 13, 20, 30, 18
5a[5..13]a[5..13]87a[5]a[7]a[5] \leftrightarrow a[7]1, 2, 4, 6, 8, 28, 16, 10, 12, 13, 20, 30, 18

前 5 輪後:[1,2,4,6,8,28,16,10,12,13,20,30,18][1, 2, 4, 6, 8, \mid 28, 16, 10, 12, 13, 20, 30, 18],左 5 個定位。被換走的元素(12、30 等)仍在未排序區等後面輪次處理。

BestAverageWorst空間穩定
Selection SortO(n2)O(n^2)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)

插入排序(Insertion Sort)

演算法策略

想像整理撲克牌:左手已排好的牌堆、右手再抽一張新的,往左找適當位置插進去。程式上把新元素 key 暫存,左邊比它大的往右推一格,直到找到不比它大的位置,塞回去。

複雜度

最佳情況(已排序)每次只比較一次 → O(n)O(n)。最壞情況(逆序)每個新元素都要推到最前面 → O(n2)O(n^2)。就地、穩定、適應性強——資料越接近有序越快,Python 的 Timsort 內部就用它處理小區段。

範例

題目[4, 1, 3, 1, 5, 2] 由小到大排序。

圖解

步驟(左邊為已排序部分):

key已排序動作結果
11[4]4 往右推、插首[1, 4, 3, 1, 5, 2]
23[1, 4]4 往右推、插 1 後[1, 3, 4, 1, 5, 2]
31[1, 3, 4]4, 3 往右推、插首後[1, 1, 3, 4, 5, 2]
45[1, 1, 3, 4]5 比 4 大、原地[1, 1, 3, 4, 5, 2]
52[1, 1, 3, 4, 5]5, 4, 3 推、插 1 後[1, 1, 2, 3, 4, 5]
Python 實作
def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
BestAverageWorst空間穩定
Insertion SortO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)

合併排序(Merge Sort)

演算法策略

分而治之:把陣列從中間切成兩半,各自遞迴排序,再把兩個已排序的小陣列合併——兩邊用指標各指一個元素,誰小誰先寫進結果陣列。一路拆到單元素(天然有序),再一路合回去。

複雜度

遞迴式 T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n),主定理 Case 2 → O(nlogn)O(n \log n)。遞迴樹 logn\log n 層,每層合計掃 nn 個元素 → nlognn \log n。不論輸入如何都一樣快。穩定、但要 O(n)O(n) 額外空間存合併結果。

範例

題目[4, 1, 3, 1, 5, 2] 由小到大排序。

圖解

步驟(分解 → 合併):

分解階段[4, 1, 3, 1, 5, 2][4, 1, 3] + [1, 5, 2] → 再切成 [4][1, 3][1][5, 2] → 最後到單元素 [4][1][3][1][5][2]

合併階段

合併過程結果
1[1] + [3]1 先、3 後[1, 3]
2[4] + [1, 3]1, 3 都比 4 小先進、4 收尾[1, 3, 4]
3[5] + [2]2 先、5 後[2, 5]
4[1] + [2, 5]1 先、2, 5 接[1, 2, 5]
5[1, 3, 4] + [1, 2, 5]雙指標合併[1, 1, 2, 3, 4, 5]
Python 實作
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)


def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    return result + left[i:] + right[j:]
BestAverageWorst空間穩定
Merge SortO(nlogn)O(n \log n)O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n)O(n)

快速排序(Quick Sort)

演算法策略

挑一個基準值(pivot),把陣列分成「比 pivot 小」、「等於 pivot」、「比 pivot 大」三堆,對左右兩堆遞迴。實務上最常用,常數係數小、cache 友好。

關鍵是 pivot 選得好不好:永遠選第一個,遇到已排序輸入時會退化成 O(n2)O(n^2)。實務用隨機選或三數取中(median-of-three)來避免 worst case。

複雜度

平均情況 pivot 把陣列切成接近均等的兩半:T(n)=2T(n/2)+O(n)=O(nlogn)T(n) = 2T(n/2) + O(n) = O(n \log n)。最壞情況 pivot 每次都是極值:T(n)=T(n1)+O(n)=O(n2)T(n) = T(n - 1) + O(n) = O(n^2)。不穩定(partition swap 會打亂相等元素順序)。

範例

題目[4, 1, 3, 1, 5, 2] 由小到大排序,選中間元素為 pivot。

圖解

步驟

回合輸入pivotLL<<EE==GG>>
1[4, 1, 3, 1, 5, 2]3[1, 1, 2][3][4, 5]
2a[1, 1, 2](遞迴 LL1[][1, 1][2]
2b[4, 5](遞迴 GG4[][4][5]

合併:[1, 1] + [2] + [3] + [4] + [5] = [1, 1, 2, 3, 4, 5]

Python 實作
def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    mid = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + mid + quick_sort(right)
例題:Quick Sort worst case 觸發

已排序輸入 [1, 2, 3, 4, 5]、固定選第一個為 pivot:

回合輸入pivotLLGG
1[1, 2, 3, 4, 5]1[][2, 3, 4, 5]
2[2, 3, 4, 5]2[][3, 4, 5]
3[3, 4, 5]3[][4, 5]
4[4, 5]4[][5]

每次只縮 1 個元素、遞迴深度 nn、總比較 n+(n1)++1=O(n2)n + (n-1) + \ldots + 1 = O(n^2)。解法:隨機 pivot 或 median-of-three。

BestAverageWorst空間穩定
Quick SortO(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n2)O(n^2)O(logn)O(\log n)

總比較

演算法BestAverageWorst空間穩定適應性
BubbleO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)
SelectionO(n2)O(n^2)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)
InsertionO(n)O(n)O(n2)O(n^2)O(n2)O(n^2)O(1)O(1)
MergeO(nlogn)O(n \log n)O(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n)O(n)
QuickO(nlogn)O(n \log n)O(nlogn)O(n \log n)O(n2)O(n^2)O(logn)O(\log n)

實務選擇:

  • 資料量小或幾乎有序 → Insertion
  • 要穩定且資料量大 → Merge
  • 一般最快 → Quick + 隨機 pivot
  • Bubble / Selection → 只拿來教學

常見考法

題型必備技能
給陣列跑前 kk列每輪比較/交換/狀態表(Bubble、Selection、Insertion 最常見)
判斷跑幾輪剛好排好Bubble 用 flag 檢查、Insertion 看已排序前綴
比較排序下界log2n!=Θ(nlogn)\lceil \log_2 n! \rceil = \Theta(n \log n)
穩定性判斷Merge/Insertion/Bubble 穩定;Selection/Quick 不穩定,要會舉反例
Quick worst case 觸發已排序 + 固定選首 pivot → O(n2)O(n^2)
Merge 合併步驟雙指標、合併成本 O(nL+nR)O(n_L + n_R)
推導 O(n2)O(n^2)i=1n1(ni)=n(n1)2\sum_{i=1}^{n-1}(n-i) = \frac{n(n-1)}{2}
推導 O(nlogn)O(n \log n)遞迴樹 logn\log n 層、每層 nnnlognn \log n

Selection 為何不穩定:「跟未排序區第一個交換」的動作會把相等元素「跳過」其他相等元素。[3a, 3b, 1] 第 1 輪 3a3a 跑到位置 3,順序就顛倒了。Quick Sort 的 partition swap 同理會破壞相等元素順序。