1960-快速排序#
案件摘要#
1960 年,年輕的英國人 Tony Hoare 在莫斯科州立大學交換,一邊學俄語、一邊為一家機器翻譯計畫打工。他遇到的第一個任務,是把俄英詞典的單詞按字母順序排好,供翻譯程式查找。當時教科書上的合併排序需要額外的磁帶與記憶體,對小機器而言過於奢侈。Hoare 在深夜的構想中發明了一種「先分割、再各個擊破」的原地排序法——快速排序,這樁懸案於 1961 年以《Algorithm 64: Quicksort》結案,成為實務上最快的通用排序。
前因 -- 為什麼會有這個案子#
- Hoare 在莫斯科交換學生,兼職替機器翻譯計畫排序俄英詞典的單詞卡。
- 合併排序需要 \(O(n)\) 的額外記憶體,當時機器記憶體極小,額外緩衝區不可行。
- Hoare 想要「原地排序(in-place)」:只用陣列本身,不需輔助空間。
- 已有的插入排序、氣泡排序雖然原地,但平均複雜度為 \(O(n^2)\) ,對大詞典太慢。
- 分治(divide and conquer)思想已在合併排序中出現,但無人能讓它「不花額外空間」。
線索與推理 -- 數學式、程式、理論#
線索一:分割(partition)#
選一個樞紐(pivot),一趟掃描把小於 pivot 的元素移到左邊、大於的移到右邊,只需 \(O(n)\) 時間與 \(O(1)\) 空間。分割完成後,pivot 已落在最終位置,左右兩半再各自遞迴。
其中 \(k\) 是左半部的大小。案件的關鍵就在 \(k\) 的分布。
線索二:平均 \(O(n \log n)\) ,最壞 \(O(n^2)\)#
若 pivot 每次都把陣列大致對半切,遞迴深度為 \(\log_2 n\) ,每層總工作量 \(O(n)\) :
但若陣列已排序而 pivot 又選首元素,每次分割只切出一個元素:
線索三:隨機 pivot —— 破案時刻#
Hoare 的修正是:不讓敵人(輸入)預測你的策略。隨機選 pivot 後,對任何輸入,比較次數的期望值為
常數 1.39 來自調和級數 \(\sum 1/i = \ln n + \gamma\) ;合併排序的比較次數約 \(n \log_2 n\) ,快排略多但實務更快(原地、快取友好)。這是隨機化演算法(randomized algorithm)的早期經典示範。
線索四:3-way partition 與重複鍵#
1962 年的完整論文中 Hoare 已處理重複鍵的改進;1976 年 Dijkstra 把它形式化為「荷蘭國旗問題」:一趟掃描把陣列分成小於、等於、大於三段,重複鍵極多時將複雜度壓回 \(O(n)\) 級。
程式碼示範#
import random
comparisons = 0
def partition(a, lo, hi):
global comparisons
p = random.randint(lo, hi)
a[p], a[hi] = a[hi], a[p]
pivot = a[hi]
i = lo - 1
for j in range(lo, hi):
comparisons += 1
if a[j] < pivot:
i += 1
a[i], a[j] = a[j], a[i]
a[i+1], a[hi] = a[hi], a[i+1]
return i + 1
def quicksort(a, lo=0, hi=None):
if hi is None:
hi = len(a) - 1
if lo < hi:
p = partition(a, lo, hi)
quicksort(a, lo, p - 1)
quicksort(a, p + 1, hi)
data = list(range(1000))
random.shuffle(data)
quicksort(data)
assert data == sorted(data)
n = len(data)
print(f"n={n}, comparisons={comparisons}")
print(f"1.39 n log2 n = {1.39 * n * __import__('math').log2(n):.0f}")
隨機 pivot 使比較次數落在 \(1.39\,n\log_2 n\) 附近,即使輸入已排序也不會退化。
結案 -- 後果與影響#
- 快速排序成為實務上最快的通用排序:原地、快取友好、程式簡潔。
- C 語言標準函式庫的
qsort、諸多語言內建排序皆以其為基礎或參考。 - 隨機化演算法的早期示範:以隨機性對抗最壞輸入,開啟 Las Vegas 演算法傳統。
- partition 思想延伸出選擇演算法:quickselect 求 \(O(n)\) 期望的第 \(k\) 小,Blum-Floyd-Pratt-Rivest-Tarjan 的 median of medians 給出最壞 \(O(n)\) 。
- Hoare 因包括快速排序在內的貢獻獲 1980 年圖靈獎。
關鍵人物與文獻#
- C.A.R. Hoare (1961). Algorithm 64: Quicksort. Communications of the ACM, 4(7), 321.
- C.A.R. Hoare (1962). Quicksort. The Computer Journal, 5(1), 10-16.
- E.W. Dijkstra (1976). A Discipline of Programming. Prentice-Hall.(荷蘭國旗問題)
- J. Blum, R. Floyd, V. Pratt, R. Rivest, R. Tarjan (1973). Time bounds for selection. Journal of Computer and System Sciences, 7(4), 448-461.
- C.A.R. Hoare (1981). The Emperor's Old Clothes. Communications of the ACM, 24(2), 75-83.(1980 圖靈獎演講)
- D.E. Knuth (1998). The Art of Computer Programming, Vol. 3: Sorting and Searching (2nd ed.). Addison-Wesley.