排序算法是程序設計的內功心法。本文以 C 語言為載體,按「認知負荷由低到高」順序,系統講解桶排序、插入排序、冒泡排序、快速排序四大經典算法,包含核心思想、代碼實現、優化技巧、時空複雜度分析及避坑指南,助你建立扎實的算法直覺。
| 算法 | 平均時間 | 最壞時間 | 空間 | 穩定性 | 適用場景 |
|---|---|---|---|---|---|
| 桶排序 | O(n+k) | O(n+k) | O(k) | 穩定 | 數據範圍小、整數、非比較類 |
| 插入排序 | O(n²) | O(n²) | O(1) | 穩定 | 小規模、近乎有序、在線數據 |
| 冒泡排序 | O(n²) | O(n²) | O(1) | 穩定 | 教學、極小數據、檢測有序性 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不穩定 | 通用大規模、內存敏感、庫函數標配 |
適用於「已知數據範圍」的整數排序。申請大小為範圍的計數陣列(桶),遍歷輸入遞增對應桶,最後按順序輸出桶索引。
#define MAXV 1000
void BucketSortBasic(int *a, int n) {
int bucket[MAXV] = {0};
for (int i = 0; i < n; ++i) bucket[a[i]]++;
int idx = 0;
for (int v = 0; v < MAXV; ++v)
while (bucket[v]--) a[idx++] = v;
}
void BucketSortOptimized(int *a, int n) {
if (n <= 1) return;
int min = a[0], max = a[0];
for (int i = 1; i < n; ++i) {
if (a[i] < min) min = a[i];
if (a[i] > max) max = a[i];
}
int range = max - min + 1;
int *bucket = (int*)calloc(range, sizeof(int));
for (int i = 0; i < n; ++i) bucket[a[i] - min]++;
int idx = 0;
for (int v = 0; v < range; ++v)
while (bucket[v]--) a[idx++] = v + min;
free(bucket);
}
統一偏移:bucket[a[i] - min]++,輸出時 v + min。calloc 自動歸零,避免棧溢出。
維護前綴有序區。從第二個元素開始,將當前元素插入已排序的前綴正確位置,類似整理撲克牌。
void InsertionSort(int *a, int n) {
for (int i = 1; i < n; ++i) {
int key = a[i];
int j = i - 1;
while (j >= 0 && a[j] > key) { // 向右挪移大於 key 的元素
a[j + 1] = a[j];
--j;
}
a[j + 1] = key; // 插入空位
}
}
O(log i) 定位,但移動元素仍 O(i),總體仍 O(n²),適合比較操作昂貴場景a[0] 作哨兵,省去 j>=0 判斷O(n^1.3)O(n) 線性時間重複掃描數組,相鄰逆序對交換,大元素像氣泡一樣「浮」到右端。每輪確定一個最大值位置。
void BubbleSort(int *a, int n) {
for (int i = 0; i < n - 1; ++i) {
for (int j = 0; j < n - 1 - i; ++j) {
if (a[j] > a[j + 1]) {
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
}
}
void BubbleSortOptimized(int *a, int n) {
int left = 0, right = n - 1;
while (left < right) {
int last_swap = left;
// 正向冒泡:最大值移到 right
for (int j = left; j < right; ++j)
if (a[j] > a[j + 1]) {
int tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp;
last_swap = j;
}
right = last_swap; // 最後交換位置後已有序
if (left >= right) break;
// 反向冒泡:最小值移到 left
last_swap = right;
for (int j = right; j > left; --j)
if (a[j - 1] > a[j]) {
int tmp = a[j]; a[j] = a[j - 1]; a[j - 1] = tmp;
last_swap = j;
}
left = last_swap;
}
}
雖實戰罕用,但完美展示「雙重循環邊界控制」、「交換邏輯」、「優化思維(記錄最後交換位置)」,是初學者理解迴圈不變量的最佳素材。
分治:選基準 → 分區(左小右大)→ 遞歸排左右子區。原地分區,空間僅遞歸棧 O(log n)。
int Partition(int *a, int low, int high) {
int pivot = a[low]; // 選首元素為基準
while (low < high) {
while (low < high && a[high] >= pivot) --high;
a[low] = a[high]; // 填坑
while (low < high && a[low] <= pivot) ++low;
a[high] = a[low]; // 填坑
}
a[low] = pivot; // 基準歸位
return low;
}
void QuickSort(int *a, int low, int high) {
if (low < high) {
int pivot = Partition(a, low, high);
QuickSort(a, low, pivot - 1);
QuickSort(a, pivot + 1, high);
}
}
因為首元素作基準並被保存,a[low] 位置空出成為第一個「坑」。若先從左掃描找到 >=pivot 填入 a[high],會覆蓋未處理的數據。先從右找 <pivot 填入 a[low],再從左找 >pivot 填入 a[high],交替填坑,最終 low==high 坑位歸還基準值,保證左側 ≤ 基準 ≤ 右側。
| 優化 | 目的 | 實現 |
|---|---|---|
| 三數取中選基準 | 避免有序/逆序數組退化 O(n²) | 取 low, mid, high 中位數,與 low 交換 |
| 小區間切換插入排序 | 減少遞歸開銷,利用插入排序小數組優勢 | if (high - low < 16) InsertionSort(a+low, high-low+1); |
| 尾遞歸消除 | 限制棧深度 O(log n) | 遞歸處理較小分區,較大分區迴圈迭代 |
| 三向切分 | 大量重複鍵值時 O(n) | Dijkstra 荷蘭國旗算法:<pivot | =pivot | >pivot |
| 隨機化基準 | 期望 O(n log n),防惡意構造 | swap(a[low], a[rand()%(high-low+1)+low]); |
void QuickSortIterative(int *a, int n) {
struct Range { int low, high; };
struct Range stack[64]; // log2(10^9) < 32
int top = 0;
stack[top++] = (struct Range){0, n-1};
while (top) {
struct Range r = stack[--top];
if (r.low >= r.high) continue;
int p = Partition(a, r.low, r.high);
// 先壓入大分區,再壓入小分區,棧深度 O(log n)
if (p - r.low > r.high - p) {
stack[top++] = (struct Range){r.low, p-1};
stack[top++] = (struct Range){p+1, r.high};
} else {
stack[top++] = (struct Range){p+1, r.high};
stack[top++] = (struct Range){r.low, p-1};
}
}
}
只需翻轉比較判斷:
a[j] > key → a[j] < keya[j] > a[j+1] → a[j] < a[j+1]>=pivot ↔ <=pivot,<=pivot ↔ >=pivotv=0..max → v=max..0或統一抽象比較器:int cmp(int x, int y) { return x - y; } 升序,return y - x; 降序,算法內部調用 cmp(a,b) > 0 判斷逆序。
n<=1 直接返回;遞歸基線 low >= highmid = low + (high - low) / 2 而非 (low+high)/2cmp(a,b) == -cmp(b,a),cmp(a,a)==0,傳遞性排序算法不僅是面試必考,更是理解「分治、遞歸、原地操作、緩存友好、分支預測」等系統級性能工程核心概念的最佳切入點。建議動手實現所有四種算法,並在 10⁴/10⁵/10⁶ 規模隨機數據上計時對比,親身感受 O(n²) 與 O(n log n) 的數量級鴻溝——這才是演算法直覺的真正來源。