1964-堆積排序與Floyd#
案件摘要#
1964 年,加拿大計算機科學家 J.W.J. Williams 在《Communications of the ACM》上發表《Algorithm 232: Heapsort》,提出以「堆積(heap)」這種資料結構實現的排序法。同年 Robert Floyd 以《Algorithm 245: Treesort 3》改良其建堆步驟,把建堆時間從 \(O(n \log n)\) 降到 \(O(n)\) ,全案臻於完美:保證 \(O(n \log n)\) 、原地、無額外記憶體。這樁案件同時解開了另一個謎題——優先佇列的高效實現,其影響遠超排序本身。
前因 -- 為什麼會有這個案子#
- 合併排序雖是 \(O(n \log n)\) ,卻需要 \(O(n)\) 的額外記憶體。
- 快速排序原地又快,但最壞情況 \(O(n^2)\) ,對某些應用(如即時系統)無法接受。
- 需要一種「保證 \(O(n \log n)\) 且原地」的排序演算法。
- 完全二元樹(complete binary tree)可以完美地塞進陣列而不需指標,這是堆積的物理基礎。
- 優先佇列(priority queue)當時缺乏高效實現,排序之外的需求早已浮現。
線索與推理 -- 數學式、程式、理論#
線索一:堆積的性質#
max-heap 是一棵完全二元樹,滿足父節點 \(\ge\) 子節點。用陣列表示,節點 \(i\) 的父節點為 \((i-1)/2\) ,子節點為 \(2i+1\) 與 \(2i+2\) ,不需任何指標:
樹高為 \(\lfloor \log_2 n \rfloor\) ,這是所有複雜度的根源。
線索二:sift-down 與 sift-up#
- sift-up(上濾):新元素放在尾端,與父節點比較、必要時上移,用於插入, \(O(\log n)\) 。
- sift-down(下濾):元素與較大的子節點比較、必要時下移,用於刪除最大值與建堆, \(O(\log n)\) 。
線索三:建堆的複雜度 —— 破案時刻#
樸素做法是逐個 sift-up 插入,總計 \(O(n \log n)\) 。Floyd 的改良是「自底向上建堆」:從最後一個非葉節點開始,對每個節點執行 sift-down。因為絕大多數節點都在底層、下濾距離極短,求和級數顯示總工作是線性的:
因為 \(\sum_h h/2^{h+1} \le 1\) ,建堆只需 \(O(n)\) ——這是本案的破案時刻。
線索四:排序流程與代價#
堆積排序 = 建堆( \(O(n)\) )+ 反覆把根與尾端交換、對根 sift-down( \(n-1\) 次,每次 \(O(\log n)\) ):
代價有二:一是交換破壞相等元素的相對順序(不穩定排序);二是 sift-down 的存取模式跨跳陣列(parent \(\to 2i+1\) ),快取局部性不如快排的順序掃描。
程式碼示範#
import random
def sift_down(a, i, size):
while True:
l, r = 2*i + 1, 2*i + 2
largest = i
if l < size and a[l] > a[largest]:
largest = l
if r < size and a[r] > a[largest]:
largest = r
if largest == i:
return
a[i], a[largest] = a[largest], a[i]
i = largest
def build_heap(a):
n = len(a)
for i in range(n // 2 - 1, -1, -1):
sift_down(a, i, n)
def heapsort(a):
n = len(a)
build_heap(a)
for end in range(n - 1, 0, -1):
a[0], a[end] = a[end], a[0]
sift_down(a, 0, end)
data = [random.randint(0, 999) for _ in range(1000)]
heapsort(data)
assert data == sorted(data)
print("heapsort 正確,n =", len(data))
build_heap 即 Floyd 的 \(O(n)\) 自底向上建堆;整體保證 \(O(n \log n)\) 。
結案 -- 後果與影響#
- 堆積成為優先佇列的標準實現:加速 Dijkstra 最短路徑(從 \(O(V^2)\) 到 \(O((V+E)\log V)\) )與 A* 搜尋。
- Python 的
heapq模組、C++ 的priority_queue皆以二元堆積為基礎。 - 堆積排序保證 \(O(n \log n)\) 且原地,成為嵌入式與即時系統、以及 introsort(快排退化時切換至堆積排序)的安全後盾。
- Floyd 的建堆分析(依層求和、節點高度分布)是攤還分析(amortized analysis)思想的先聲。
- Robert W. Floyd 因在演算法分析方面的貢獻獲 1978 年圖靈獎(其 1962 年的 Floyd-Warshall 全源最短路徑演算法亦是經典)。
關鍵人物與文獻#
- J.W.J. Williams (1964). Algorithm 232: Heapsort. Communications of the ACM, 7(6), 347-338.
- R.W. Floyd (1964). Algorithm 245: Treesort 3. Communications of the ACM, 7(12), 701.
- R.W. Floyd (1962). Algorithm 97: Shortest Path. Communications of the ACM, 5(6), 345.
- T.H. Cormen, C.E. Leiserson, R.L. Rivest, C. Stein (2022). Introduction to Algorithms (4th ed.). MIT Press.(第六章:Heapsort)
- R.W. Floyd (1978). The Paradigms of Programming. Communications of the ACM, 22(8), 455-460.(1978 圖靈獎演講)
- D.E. Knuth (1998). The Art of Computer Programming, Vol. 3: Sorting and Searching (2nd ed.). Addison-Wesley.(5.2.3 節)