進階排序:O(n log n) 排序演算法#
歸併排序與快速排序是處理大規模資料的主力演算法,兩者都運用分治思想,但策略截然不同。
分治思想#
將大問題分解為小問題,解決小問題後合併結果。
flowchart TB
subgraph 歸併排序["歸併排序:由下而上"]
direction TB
M1[分解陣列] --> M2[遞迴排序左半]
M1 --> M3[遞迴排序右半]
M2 --> M4[合併結果]
M3 --> M4
end
subgraph 快速排序["快速排序:由上而下"]
direction TB
Q1[選擇 Pivot] --> Q2[分區操作]
Q2 --> Q3[遞迴排序左區]
Q2 --> Q4[遞迴排序右區]
end
style M4 fill:#e8f5e9
style Q2 fill:#fff3e0T(n) = 2*T(n/2) + O(n) => T(n) = O(n log n)歸併排序 (Merge Sort)#
由下而上:先遞迴處理子問題,再合併結果。
核心流程#
- 將陣列從中間分成兩半
- 對左右兩半分別遞迴排序
- 合併兩個有序陣列
| 特性 | 說明 |
|---|---|
| 時間複雜度 | 穩定 O(n log n),不受輸入影響 |
| 空間複雜度 | O(n),需要臨時陣列 |
| 穩定性 | 穩定 |
歸併排序程式碼
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
result.extend(left[i:])
result.extend(right[j:])
return result歸併排序的空間複雜度是 O(n) 而非 O(n log n)。雖然遞迴層數是 log n,但同一時間只有一個函式在執行,臨時空間會被釋放重用。
快速排序 (Quick Sort)#
由上而下:先分區,再遞迴處理子問題。
核心流程#
- 選擇一個 pivot(分區點)
- 將小於 pivot 的放左邊,大於 pivot 的放右邊
- 對左右兩部分遞迴排序
| 特性 | 說明 |
|---|---|
| 時間複雜度 | 平均 O(n log n),最壞 O(n²) |
| 空間複雜度 | O(log n),原地排序 |
| 穩定性 | 不穩定 |
快速排序程式碼(原地分區)
def quick_sort(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
pivot_idx = partition(arr, low, high)
quick_sort(arr, low, pivot_idx - 1)
quick_sort(arr, pivot_idx + 1, high)
return arr
def partition(arr, low, high):
pivot = arr[high] # 選最後一個元素作為 pivot
i = low
for j in range(low, high):
if arr[j] < pivot:
arr[i], arr[j] = arr[j], arr[i]
i += 1
arr[i], arr[high] = arr[high], arr[i]
return i快排優化策略#
問題:最壞情況 O(n²)#
當資料已有序且每次選最後一個元素作為 pivot 時,分區極度不均衡。
解決方案#
- 三數取中法:取首、中、尾三個數的中位數作為 pivot
- 隨機法:隨機選擇 pivot,降低最壞情況發生的概率
- 避免遞迴過深:設定遞迴深度閾值,或使用堆上的堆疊模擬遞迴
快排 vs 歸併
比較項目 歸併排序 快速排序 時間穩定性 穩定 O(n log n) 平均 O(n log n),可能退化 空間複雜度 O(n) O(log n) 實際應用 較少(記憶體消耗大) 較多(原地、常數項小)
經典應用:O(n) 查找第 K 大元素#
利用快排的分區思想,可以在 O(n) 時間內找到無序陣列中的第 K 大元素。
第 K 大元素程式碼
def find_kth_largest(arr, k):
def partition(low, high):
pivot = arr[high]
i = low
for j in range(low, high):
if arr[j] >= pivot: # 降序分區
arr[i], arr[j] = arr[j], arr[i]
i += 1
arr[i], arr[high] = arr[high], arr[i]
return i
low, high = 0, len(arr) - 1
while low <= high:
pivot_idx = partition(low, high)
if pivot_idx == k - 1:
return arr[pivot_idx]
elif pivot_idx < k - 1:
low = pivot_idx + 1
else:
high = pivot_idx - 1
return -1為何是 O(n)?每次只需處理一半:n + n/2 + n/4 + … = 2n = O(n)
工業級排序函式實作(以 C 語言 qsort 為例)#
- 小資料量:使用歸併排序(空間換時間可接受)
- 大資料量:使用快速排序
- 分區點選擇:三數取中法
- 遞迴優化:堆上模擬堆疊,避免堆疊溢出
- 小區間優化:元素 <= 4 時切換為插入排序