cover

上一篇講了五個基礎排序。這篇講的是「特殊場景的排序」——當你的資料有特殊性質時,可以比 O(n log n) 更快。以及 Tim Sort,你每天都在用但可能不知道的排序演算法。

先講結論

比較排序的理論下界是 O(n log n),沒辦法更快了。但如果你的資料是整數、值域不大、或均勻分佈,非比較排序可以做到 O(n)。

演算法時間空間穩定適用
Heap SortO(n log n)O(1)記憶體受限
Counting SortO(n + k)O(k)整數,值域小
Radix SortO(d(n+k))O(n+k)整數,位數固定
Bucket SortO(n + k)O(n)均勻分佈
Shell SortO(n^1.3)O(1)中等規模
Tim SortO(n log n)O(n)通用(內建排序)
Cocktail SortO(n²) / O(n) bestO(1)幾乎有序、Bubble 改良
Comb SortO(n log n) avgO(1)Bubble 的間隔版
Cycle SortO(n²)O(1)寫入次數最少
IntrosortO(n log n)O(log n)C++ STL 的選擇

Heap Sort:空間最省的 O(n log n)

利用 max heap 的性質:根節點永遠最大。每次取出根(最大值)放到陣列尾端,再調整堆。

void heapSort(int[] arr) {
    int n = arr.length;
    for (int i = n/2-1; i >= 0; i--)
        heapify(arr, n, i);         // 建堆 O(n)
    for (int i = n-1; i > 0; i--) {
        swap(arr, 0, i);            // 最大值放後面
        heapify(arr, i, 0);         // 調整 O(log n)
    }
}

O(1) 額外空間,但 cache locality 差(heap 的存取模式跳來跳去),所以實務上比 Quick Sort 慢。什麼時候用?記憶體極度受限或需要保證最差 O(n log n)(Quick Sort 最差是 O(n²))。

Counting Sort:不比較,純計數

如果值域是 0~k,開一個大小 k 的陣列統計每個值出現幾次,然後按順序輸出。

[4, 2, 2, 8, 3, 3, 1]
count: [0, 1, 2, 2, 1, 0, 0, 0, 1]
結果:  [1, 2, 2, 3, 3, 4, 8]

O(n + k)。但如果值域很大(比如排序身分證字號),開不了那麼大的陣列。

Radix Sort:按位數排

從低位到高位,每一位用 Counting Sort。

[170, 45, 75, 90, 802, 24, 2, 66]
按個位: [170, 90, 802, 2, 24, 45, 75, 66]
按十位: [802, 2, 24, 45, 66, 170, 75, 90]
按百位: [2, 24, 45, 66, 75, 90, 170, 802]

O(d × (n + k)),d 是位數。適合整數且位數固定的場景。

Bucket Sort:分桶再排

把數據分到幾個桶裡,每個桶內部排序,再合併。

如果數據均勻分佈,每個桶裡的元素很少,桶內排序幾乎是 O(1),整體接近 O(n)。不均勻的話就退化成 O(n²)。

Tim Sort:你一直在用的排序

Python 的 sort() 和 Java 的 Arrays.sort()(Object 版)用的就是 Tim Sort。

核心想法:現實世界的資料很少是完全亂序的,通常有一些「已排序的片段」。Tim Sort 先找出這些片段(run),小片段用 Insertion Sort,再用 Merge Sort 合併。

  • 近乎有序?O(n)
  • 一般情況?O(n log n)
  • 穩定?是

所以你不需要自己挑排序演算法——語言內建的已經幫你選了 Tim Sort。除非你有特殊需求(整數值域小 → Counting Sort、記憶體受限 → Heap Sort),否則直接 .sort() 就好。

Cocktail Sort:雙向 Bubble

Bubble Sort 每輪只從左掃到右——大的很快浮右邊(兔子),但小的從右往左移極慢(烏龜)。Cocktail Sort 每輪來回掃,一次把最大值推右、最小值推左,烏龜一輪就能歸位。

[5, 1, 4, 2, 8, 0, 2]
→ 左到右:8 到底   [1, 4, 2, 5, 0, 2, 8]
← 右到左:0 到頭   [0, 1, 4, 2, 5, 2, 8]
→ 縮小範圍繼續...

穩定、原地。複雜度跟 Bubble Sort 一樣是 O(n²),但幾乎有序時快一些。

Comb Sort:梳子梳掉烏龜

Cocktail Sort 每次只移動一格,根治不了問題。Comb Sort 的想法更像 Shell Sort:從大間隔開始比較,把遠端的烏龜提前消滅,間隔每輪縮小 1.3 倍,最後 gap=1 時等同 Bubble Sort,但此時幾乎已沒有烏龜。

int gap = arr.length;
boolean swapped = true;
while (gap > 1 || swapped) {
    gap = Math.max(1, (int)(gap / 1.3));
    swapped = false;
    for (int i = 0; i + gap < arr.length; i++)
        if (arr[i] > arr[i + gap]) { swap(i, i+gap); swapped = true; }
}

不穩定,平均接近 O(n log n),實作極簡單。競賽中偶爾用來替代 Bubble Sort。

Cycle Sort:寫入次數最少

每個元素找到它的正確位置(比它小的元素有幾個),直接放過去。這樣每個元素最多被寫入一次,整個排序的寫入次數是 O(n)。

為什麼重要?快閃記憶體(NAND Flash)每個位置的寫入壽命有限,寫入次數少 = 硬體壽命更長。時間複雜度 O(n²),不穩定,原地。你不會在一般情景用它,但硬體相關的排序場景會碰到。

Introsort:C++ 的選擇

C++ 的 std::sort 用的是 Introsort,而不是 Tim Sort。原因是它更適合原始型別(int、double)——不需要穩定性,只要快。

策略:用 Quick Sort 起手(最快),遞迴深度超過 2×log₂(n) 就切換到 Heap Sort(防止最差情況),子陣列小於 16 個元素改用 Insertion Sort(小陣列開銷最低)。

Quick Sort → 深度過深? → Heap Sort
                          子陣列夠小? → Insertion Sort

保證 O(n log n) 最差情況,O(log n) 額外空間,不穩定。Java/Python 內建用 Tim Sort(穩定);C++ 用 Introsort(快)。兩個選擇都是對的,只是優先考量不同。

怎麼選?

你的資料是什麼?
├─ 整數,值域小 → Counting Sort
├─ 整數,位數固定 → Radix Sort
├─ 浮點數,均勻分佈 → Bucket Sort
└─ 通用資料
   ├─ 記憶體極度受限 → Heap Sort
   ├─ 幾乎有序,需要穩定 → Tim Sort / Cocktail Sort
   ├─ 幾乎有序,不需穩定 → Comb Sort
   ├─ 寫入代價高(Flash)→ Cycle Sort
   └─ 一般情況 → Quick Sort / 直接 .sort()

最好的排序演算法是什麼?答:你語言內建的那個。Tim Sort(Java/Python)和 Introsort(C++)已經幫你做了所有決策。除非你在面試或有硬體限制,否則請直接用 .sort()