歸並排序是分治法的另一經典應用,具有穩定 O(n log n) 時間複雜度、穩定排序特性,是外部排序與連結清單排序的首選。本文詳細解析自頂向下遞迴版、自底向上迭代版、自然歸並排序與 C++ 實作細節。
MergeSort(A, left, right):
if left < right:
mid = (left + right) / 2
MergeSort(A, left, mid)
MergeSort(A, mid+1, right)
Merge(A, left, mid, right)
template <class T>
void Merge(T c[], T d[], int l, int m, int r) {
int i = l, j = m + 1, k = l;
while (i <= m && j <= r) {
if (c[i] <= c[j]) d[k++] = c[i++];
else d[k++] = c[j++];
}
while (i <= m) d[k++] = c[i++];
while (j <= r) d[k++] = c[j++];
}
template <class T>
void MergeSort(T a[], int n) {
T *b = new T[n];
mergeSort(a, b, 0, n-1);
delete[] b;
}
template <class T>
void mergeSort(T a[], T b[], int l, int r) {
if (l >= r) return;
int m = (l + r) / 2;
mergeSort(a, b, l, m);
mergeSort(a, b, m+1, r);
merge(a, b, l, m, r);
copy(b + l, b + r + 1, a + l); // 複製回原陣列
}
不使用遞迴,而是按段長 1、2、4、8... 依次合併相鄰有序段:
template <class T>
void MergeSort(T a[], int n) {
T *b = new T[n];
for (int seg = 1; seg < n; seg *= 2) {
mergePass(a, b, seg, n); // a → b
seg *= 2;
if (seg < n) mergePass(b, a, seg, n); // b → a
}
delete[] b;
}
template <class T>
void mergePass(T x[], T y[], int seg, int n) {
int i = 0;
while (i <= n - 2*seg) {
merge(x, y, i, i+seg-1, i+2*seg-1);
i += 2*seg;
}
if (i + seg < n) // 剩餘兩段不等長
merge(x, y, i, i+seg-1, n-1);
else // 僅剩一段
copy(x + i, x + n, y + i);
}
利用輸入資料中已存在的有序子序列,減少合併輪數。
輸入:[4, 8, 3, 7, 1, 5, 6, 2]
自然有序段:[4,8] [3,7] [1,5,6] [2] → 4 段
第 1 輪:[3,4,7,8] [1,2,5,6] → 2 段
第 2 輪:[1,2,3,4,5,6,7,8] → 完成
標準歸並需 3 輪(log₂8),自然歸並僅 2 輪。
| 版本 | 時間複雜度 | 空間複雜度 | 穩定性 | 適用場景 |
|---|---|---|---|---|
| 遞迴版 | O(n log n) | O(n) + O(log n)棧 | 穩定 | 一般用途、教學 |
| 迭代版 | O(n log n) | O(n) | 穩定 | 大規模資料、嵌入式系統 |
| 自然歸並 | O(n log n) 最佳 O(n) | O(n) | 穩定 | 部分有序資料、外部排序 |
int mid = l + (r - l) / 2; // 而非 (l+r)/2
標準歸並需要 O(n) 輔助空間。就地合併(In-place merge)可將空間降為 O(1),但實作複雜且常數因子大,實務較少用。
鏈結清單無需輔助陣列,合併時只需調整指標,空間 O(1):
ListNode* merge(ListNode* a, ListNode* b) {
ListNode dummy(0), *tail = &dummy;
while (a && b) {
if (a->val <= b->val) tail->next = a, a = a->next;
else tail->next = b, b = b->next;
tail = tail->next;
}
tail->next = a ? a : b;
return dummy.next;
}
資料量超過記憶體時,歸並排序是標準解法:
| 演算法 | 平均 | 最壞 | 最佳 | 空間 | 穩定 | 備註 |
|---|---|---|---|---|---|---|
| 歸並排序 | n log n | n log n | n log n | n | ✓ | 穩定、外部排序首選 |
| 快速排序 | n log n | n² | n log n | log n | ✗ | 原地、緩存友好 |
| 堆積排序 | n log n | n log n | n log n | 1 | ✗ | 原地、最壞情況保證 |
| 插入排序 | n² | n² | n | 1 | ✓ | 小陣列、近乎有序 |
Python、Java、Android 使用的 Timsort 結合歸並與插入排序:
#include <algorithm>
#include <vector>
std::vector<int> v = {5, 2, 8, 1, 9};
std::stable_sort(v.begin(), v.end()); // 保證穩定,通常用歸並排序
歸並排序以穩定、可預測的 O(n log n) 效能、天然適合外部排序與連結結構,成為排序演算法中的基石。理解其遞迴與迭代實作、自然歸並優化、以及與快速排序的權衡,是演算法工程師的必修功課。