演算法概念
排序就是把一堆資料照大小排好,好處是之後查詢、去重、找中位數都便宜。評估一個排序演算法要看四件事:時間(多快)、空間(要不要多開陣列)、穩定性(相等元素順序會不會被打亂)、適應性(資料已經接近有序時會不會更快)。
比較型排序(靠比誰大誰小決定順序)的理論下界是 ,因為 種排列要用二元判斷樹區分,至少要 次比較。想突破這個下界只能用非比較型(counting/radix/bucket),但那些有額外假設。
穩定性的例子:[('Alice', 90), ('Bob', 80), ('Charlie', 90)] 依分數升序,Alice 和 Charlie 同分,穩定排序後 Alice 仍在 Charlie 前面。
各適用情境
泡泡排序(Bubble Sort)
演算法策略
相鄰兩個比一比,大的往右推。一輪下來最大的值就冒到最右定位。每輪少掃一個(右邊已定位的不用再比),直到某一輪沒發生任何交換就提早結束。
變數: 是外層輪次(代表右邊已有 個定位), 是內層指標從 0 掃到 。
複雜度
最壞每輪掃 次 → 。最佳情況(已排序)一輪就發現沒 swap、提早結束 → 。就地、穩定。
範例
題目:[4, 1, 3, 1, 5, 2] 由小到大排序。
圖解:
步驟:
第 1 輪(): 掃 0 到 4。
arr[j] | arr[j+1] | 動作 | 陣列 | |
|---|---|---|---|---|
| 0 | 4 | 1 | swap | [1, 4, 3, 1, 5, 2] |
| 1 | 4 | 3 | swap | [1, 3, 4, 1, 5, 2] |
| 2 | 4 | 1 | swap | [1, 3, 1, 4, 5, 2] |
| 3 | 4 | 5 | — | [1, 3, 1, 4, 5, 2] |
| 4 | 5 | 2 | swap | [1, 3, 1, 4, 2, 5] |
5 冒到最右。
第 2 輪(): 掃 0 到 3。
arr[j] | arr[j+1] | 動作 | 陣列 | |
|---|---|---|---|---|
| 0 | 1 | 3 | — | [1, 3, 1, 4, 2, 5] |
| 1 | 3 | 1 | swap | [1, 1, 3, 4, 2, 5] |
| 2 | 3 | 4 | — | [1, 1, 3, 4, 2, 5] |
| 3 | 4 | 2 | swap | [1, 1, 3, 2, 4, 5] |
第 3 輪(): 掃 0 到 2。
arr[j] | arr[j+1] | 動作 | 陣列 | |
|---|---|---|---|---|
| 0 | 1 | 1 | — | [1, 1, 3, 2, 4, 5] |
| 1 | 1 | 3 | — | [1, 1, 3, 2, 4, 5] |
| 2 | 3 | 2 | swap | [1, 1, 2, 3, 4, 5] |
第 4 輪():全程無 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 輪只在 交換 5, 4 → [1, 2, 3, 4, 5],但本輪有 swap。第 2 輪全程無 swap、提早結束。所以跑 2 輪。
| Best | Average | Worst | 空間 | 穩定 | |
|---|---|---|---|---|---|
| Bubble Sort | ✓ |
選擇排序(Selection Sort)
演算法策略
每輪從未排序區找最小值,跟未排序區的第一個位置交換。左邊已排序區每輪長一格,右邊縮一格。
變數: 是外層輪次(也是未排序區起點)、 從 掃到末端、min_idx 記當輪最小值位置。
複雜度
無論輸入如何,外層 輪、內層每輪平均掃 次 → 一律 。好處是交換次數最少(每輪一次),壞處是不穩定——[3a, 3b, 1] 第 1 輪把 換到位置 3,就跳到 後面了。
範例
題目:[4, 1, 3, 1, 5, 2] 由小到大排序。
圖解:
步驟:
| 輪 | 未排序區 | 最小值 | 位置 | 交換 | 交換後陣列 |
|---|---|---|---|---|---|
| 0 | 全部 | 1 | 1 | [1, 4, 3, 1, 5, 2] | |
| 1 | [4,3,1,5,2] | 1 | 3 | [1, 1, 3, 4, 5, 2] | |
| 2 | [3,4,5,2] | 2 | 5 | [1, 1, 2, 4, 5, 3] | |
| 3 | [4,5,3] | 3 | 5 | [1, 1, 2, 3, 5, 4] | |
| 4 | [5,4] | 4 | 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 輪
陣列 ,用 1-indexed,跑前 5 輪。
| 輪 | 未排序區 | 最小值 | 位置 | 交換 | 陣列 |
|---|---|---|---|---|---|
| 1 | 1 | 9 | 1, 30, 8, 2, 16, 28, 4, 10, 12, 13, 20, 6, 18 | ||
| 2 | 2 | 4 | 1, 2, 8, 30, 16, 28, 4, 10, 12, 13, 20, 6, 18 | ||
| 3 | 4 | 7 | 1, 2, 4, 30, 16, 28, 8, 10, 12, 13, 20, 6, 18 | ||
| 4 | 6 | 12 | 1, 2, 4, 6, 16, 28, 8, 10, 12, 13, 20, 30, 18 | ||
| 5 | 8 | 7 | 1, 2, 4, 6, 8, 28, 16, 10, 12, 13, 20, 30, 18 |
前 5 輪後:,左 5 個定位。被換走的元素(12、30 等)仍在未排序區等後面輪次處理。
| Best | Average | Worst | 空間 | 穩定 | |
|---|---|---|---|---|---|
| Selection Sort | ✗ |
插入排序(Insertion Sort)
演算法策略
想像整理撲克牌:左手已排好的牌堆、右手再抽一張新的,往左找適當位置插進去。程式上把新元素 key 暫存,左邊比它大的往右推一格,直到找到不比它大的位置,塞回去。
複雜度
最佳情況(已排序)每次只比較一次 → 。最壞情況(逆序)每個新元素都要推到最前面 → 。就地、穩定、適應性強——資料越接近有序越快,Python 的 Timsort 內部就用它處理小區段。
範例
題目:[4, 1, 3, 1, 5, 2] 由小到大排序。
圖解:
步驟(左邊為已排序部分):
| 抽 | key | 已排序 | 動作 | 結果 |
|---|---|---|---|---|
| 1 | 1 | [4] | 4 往右推、插首 | [1, 4, 3, 1, 5, 2] |
| 2 | 3 | [1, 4] | 4 往右推、插 1 後 | [1, 3, 4, 1, 5, 2] |
| 3 | 1 | [1, 3, 4] | 4, 3 往右推、插首後 | [1, 1, 3, 4, 5, 2] |
| 4 | 5 | [1, 1, 3, 4] | 5 比 4 大、原地 | [1, 1, 3, 4, 5, 2] |
| 5 | 2 | [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| Best | Average | Worst | 空間 | 穩定 | |
|---|---|---|---|---|---|
| Insertion Sort | ✓ |
合併排序(Merge Sort)
演算法策略
分而治之:把陣列從中間切成兩半,各自遞迴排序,再把兩個已排序的小陣列合併——兩邊用指標各指一個元素,誰小誰先寫進結果陣列。一路拆到單元素(天然有序),再一路合回去。
複雜度
遞迴式 ,主定理 Case 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] → 最後到單元素 [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:]| Best | Average | Worst | 空間 | 穩定 | |
|---|---|---|---|---|---|
| Merge Sort | ✓ |
快速排序(Quick Sort)
演算法策略
挑一個基準值(pivot),把陣列分成「比 pivot 小」、「等於 pivot」、「比 pivot 大」三堆,對左右兩堆遞迴。實務上最常用,常數係數小、cache 友好。
關鍵是 pivot 選得好不好:永遠選第一個,遇到已排序輸入時會退化成 。實務用隨機選或三數取中(median-of-three)來避免 worst case。
複雜度
平均情況 pivot 把陣列切成接近均等的兩半:。最壞情況 pivot 每次都是極值:。不穩定(partition swap 會打亂相等元素順序)。
範例
題目:[4, 1, 3, 1, 5, 2] 由小到大排序,選中間元素為 pivot。
圖解:
步驟:
| 回合 | 輸入 | pivot | () | () | () |
|---|---|---|---|---|---|
| 1 | [4, 1, 3, 1, 5, 2] | 3 | [1, 1, 2] | [3] | [4, 5] |
| 2a | [1, 1, 2](遞迴 ) | 1 | [] | [1, 1] | [2] |
| 2b | [4, 5](遞迴 ) | 4 | [] | [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:
| 回合 | 輸入 | pivot | ||
|---|---|---|---|---|
| 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 個元素、遞迴深度 、總比較 。解法:隨機 pivot 或 median-of-three。
| Best | Average | Worst | 空間 | 穩定 | |
|---|---|---|---|---|---|
| Quick Sort | ✗ |
總比較
| 演算法 | Best | Average | Worst | 空間 | 穩定 | 適應性 |
|---|---|---|---|---|---|---|
| Bubble | ✓ | ✓ | ||||
| Selection | ✗ | ✗ | ||||
| Insertion | ✓ | ✓ | ||||
| Merge | ✓ | ✗ | ||||
| Quick | ✗ | ✗ |
實務選擇:
- 資料量小或幾乎有序 → Insertion
- 要穩定且資料量大 → Merge
- 一般最快 → Quick + 隨機 pivot
- Bubble / Selection → 只拿來教學
常見考法
| 題型 | 必備技能 |
|---|---|
| 給陣列跑前 輪 | 列每輪比較/交換/狀態表(Bubble、Selection、Insertion 最常見) |
| 判斷跑幾輪剛好排好 | Bubble 用 flag 檢查、Insertion 看已排序前綴 |
| 比較排序下界 | |
| 穩定性判斷 | Merge/Insertion/Bubble 穩定;Selection/Quick 不穩定,要會舉反例 |
| Quick worst case 觸發 | 已排序 + 固定選首 pivot → |
| Merge 合併步驟 | 雙指標、合併成本 |
| 推導 | |
| 推導 | 遞迴樹 層、每層 → |
Selection 為何不穩定:「跟未排序區第一個交換」的動作會把相等元素「跳過」其他相等元素。[3a, 3b, 1] 第 1 輪 跑到位置 3,順序就顛倒了。Quick Sort 的 partition swap 同理會破壞相等元素順序。
