跳轉到

1960 - Hoare 快速排序(Quicksort 的誕生)#

案件摘要#

1960 年前後,25 歲的 Tony Hoare(1934–2025) 在莫斯科國立大學做交換研究, 為了機器翻譯專案中的一個小需求——「先把單字排序,再查字典」—— 發明了快速排序(Quicksort):

\[ \boxed{\text{選一個樞紐,小的放左邊、大的放右邊,再遞迴兩側}} \]

它的核心是一行不變量:

\[ \text{分割後:} a[lo..i] \le pivot \le a[i+1..hi]. \]

平均複雜度 \(O(n \log n)\) ,且是原地排序(in-place,不需額外記憶體)—— 這個「順手解決的小問題」,後來成為 C 語言標準函式庫的 qsort, 並隨 UNIX 散布到全世界。1980 年,Hoare 獲圖靈獎—— 而獲獎理由的開頭,正是這個在莫斯科冬夜裡想出來的演算法。

前因 -- 為什麼會有這個案子#

  • 1959 Dijkstra 最短路徑: Hoare 抵達莫斯科的前一年,Dijkstra 剛發表最短路徑演算法 (見 1959-迪傑斯特拉最短路徑.md)—— 歐洲的「演算法黃金年代」正在展開,分治法(divide and conquer) 的思想已經成熟:
\[ \text{大問題} = \text{小問題(拆)} + \text{合併(解)}. \]
  • 1957 FORTRAN: Backus 的 FORTRAN 編譯器(見 1957-巴克斯FORTRAN.md) 證明了高階語言可以自動編譯成高效的機器碼—— 程式設計師開始關心:編譯器內部的排序、查表,要怎麼做才快?
  • 當時的排序主流是 merge sort(合併排序):
  • von Neumann(1945) 早已寫下 merge sort——恆定 \(O(n \log n)\) 、穩定;
  • 但 merge sort 需要 \(O(n)\) 的額外記憶體——在 1960 年, 記憶體以 KB 計價,每一個位元組都是錢!
\[ \text{merge sort:恆 } O(n\log n) \text{,但要 } O(n) \text{ 記憶體(太貴)}. \]
  • 莫斯科的翻譯難題(1959–1960): Hoare 在莫斯科國立大學研究機器翻譯:把俄文句子翻成英文,需要查片語字典—— 但字典要先排序,才能用二分搜尋快速查詢。 他先實作了 merge sort,發現太慢也太佔記憶體;接著他嘗試一個瘋狂的想法:
\[ \boxed{\text{「不合并了——直接在原地,把陣列一切兩半!」}} \]

這一「切」,就是快速排序的誕生。

線索與推理 -- 數學式、程式、理論#

第一條線索:分治法(divide and conquer)#

快速排序的三步驟: 1. 分割(partition):選一個樞紐(pivot),把陣列分成兩半—— 小於樞紐的放左邊,大於的放右邊; 2. 遞迴(recurse):對左半、右半各自再做同樣的事; 3. 組合:什麼都不用做!分割完,兩半就地接起來就是有序的。

\[ \text{quicksort}(a, lo, hi) = \text{partition} + \text{quicksort}(a, lo, p{-}1) + \text{quicksort}(a, p{+}1, hi). \]

與 merge sort 的最大差別:merge sort 是「先遞迴、後合併(費工)」, quicksort 是「先分割(省工)、後遞迴、零合併」。

第二條線索:分割的不變量(partition invariant)#

不變量:掃描過程中,永遠維持

\[ a[lo..i] \le pivot \quad\wedge\quad a[i+1..j{-}1] > pivot \quad\wedge\quad a[j..hi] \text{ 未掃描}. \]

掃描到 \(j\) 時:若 \(a[j] \le pivot\) ,則 \(i{+}{=}1\) 並交換 \(a[i], a[j]\) ——不變量恢復。 掃描結束後,把樞紐換到 \(i{+}1\) 的位置:

\[ \boxed{a[lo..i] \le pivot \le a[i+1..hi]\quad\text{(樞紐已就位,永不移動)}} \]

樞紐一旦就位,就再也不動——每一層遞迴,至少一個元素(樞紐)到達最終位置。 這個不變量,就是快速排序正確性的全部證明。

第三條線索:平均 \(O(n \log n)\) ,最壞 \(O(n^2)\) ,與隨機化#

遞迴式:若每次分割都接近對半,則

\[ T(n) = 2T(n/2) + O(n) \;\Rightarrow\; T(n) = O(n \log n). \]

(與 merge sort 相同——但 quicksort 的分割只需一趟掃描,常數更小、記憶體 \(O(\log n)\) 只存遞迴棧。) 最壞情況:若樞紐永遠選到最大或最小(例如已排序陣列 + 固定取最後一個),分割變成 \(1 : n{-}1\) :

\[ T(n) = T(n{-}1) + O(n) \;\Rightarrow\; T(n) = O(n^2). \]

Hoare 的解法:隨機化樞紐——每次隨機挑一個元素當樞紐, 沒有人(包括輸入資料)能預測樞紐是誰,期望複雜度回到

\[ \boxed{\text{隨機化後:} \mathbb{E}[T(n)] = O(n \log n)\quad\text{(期望,對任何輸入)}} \]

這是「隨機化演算法」的經典示範——用骰子對抗最壞情況。

Python:快速排序(原地分割 + 隨機樞紐),對照 merge sort#

# 1) in-place partition + 隨機樞紐的快速排序
import random

def quicksort(a, lo=0, hi=None):
    """Hoare 式快速排序:原地排序(in-place),隨機選樞紐"""
    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)
    return a

def partition(a, lo, hi):
    """分割:選樞紐,使左邊 <= 樞紐 <= 右邊(不變量)"""
    r = random.randint(lo, hi)      # 隨機樞紐:對付已排序的「最壞情況」
    a[r], a[hi] = a[hi], a[r]
    pivot = a[hi]
    i = lo - 1                      # i:小於樞紐區的右邊界
    for j in range(lo, hi):
        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

# 2) 對照組:merge sort(穩定 O(n log n),但需要額外記憶體)
def merge_sort(a):
    if len(a) <= 1:
        return a
    mid = len(a) // 2
    L, R = merge_sort(a[:mid]), merge_sort(a[mid:])
    out, i, j = [], 0, 0
    while i < len(L) and j < len(R):
        if L[i] <= R[j]:
            out.append(L[i]); i += 1
        else:
            out.append(R[j]); j += 1
    return out + L[i:] + R[j:]

# 3) 測試:隨機資料、已排序資料(最壞情況)、與 merge sort 比對
random.seed(1960)
rand_data = [random.randint(0, 99) for _ in range(15)]
sorted_data = list(range(1, 16))          # 已排序:固定樞紐會退化成 O(n²)

print("隨機樞紐快速排序 vs merge sort")
print(f"  原始(隨機):{rand_data}")
print(f"  quicksort  :{quicksort(rand_data[:])}")
print(f"  merge sort :{merge_sort(rand_data)}")

print("\n已排序資料(最壞情況的誘餌,隨機樞紐化解套):")
print(f"  原始:{sorted_data}")
print(f"  quicksort:{quicksort(sorted_data[:])}")

ok = all(quicksort(rand_data[:]) == sorted(rand_data)
         for _ in range(1000))
print(f"\n隨機資料驗證 1000 次,全部正確?{ok}")

print("\n複雜度對照:")
print("  遞迴式:T(n) = 2T(n/2) + O(n)  →  T(n) = O(n log n)(平均)")
print("  最壞(已排序 + 固定樞紐):O(n²);隨機化後期望 O(n log n)")
print("  merge sort:恆 O(n log n),但需 O(n) 額外記憶體")
輸出:
隨機樞紐快速排序 vs merge sort
  原始(隨機):[34, 91, 48, 80, 42, 43, 58, 51, 62, 76, 66, 52, 52, 45, 95]
  quicksort  :[34, 42, 43, 45, 48, 51, 52, 52, 58, 62, 66, 76, 80, 91, 95]
  merge sort :[34, 42, 43, 45, 48, 51, 52, 52, 58, 62, 66, 76, 80, 91, 95]

已排序資料(最壞情況的誘餌,隨機樞紐化解套):
  原始:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
  quicksort:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

隨機資料驗證 1000 次,全部正確?True

複雜度對照:
  遞迴式:T(n) = 2T(n/2) + O(n)  →  T(n) = O(n log n)(平均)
  最壞(已排序 + 固定樞紐):O(n²);隨機化後期望 O(n log n)
  merge sort:恆 O(n log n),但需 O(n) 額外記憶體

結案 -- 後果與影響#

  • C 語言標準函式庫的 qsort: 1970 年代末,quicksort 成為 C 標準函式庫的排序函式 qsort—— 至今仍是最通用的排序介面(ANSI C, 1989 標準化)。
\[ \text{莫斯科的翻譯需求(1960)} \xrightarrow{} \text{全世界的標準函式庫}. \]
  • UNIX 的散布(1973): UNIX(見 1973-UNIX作業系統.md) 以 C 語言重寫後廣為流傳,qsort 隨之進入每一台電腦—— Kernighan 與 Ritchie 的《C 程式設計語言》(1978) 也用 quicksort 作為遞迴的教科書範例。
  • Knuth 的定位(1968): Knuth 在《The Art of Computer Programming》第三卷 (見 1968-克努特演算法藝術.md) 給 quicksort 完整的數學分析——稱它為二十世紀最優雅的演算法之一。
\[ \text{平均比較次數} = 2n\ln n \approx 1.39\, n\log_2 n\quad\text{(僅比理論下限多 39\%)}. \]
  • Hoare 的其他貢獻——「十億美元的錯誤」: 1965 年,Hoare 在 ALGOL W 語言中引入空引用(null reference),並於 2009 年公開懺悔:
\[ \boxed{\text{「它造成了無數的錯誤與漏洞——我的『十億美元的錯誤』」}} \]

今日所有語言的安全設計(如 Rust 的 Option、Swift 的 Optional) 都是在為這個錯誤還債。 - Hoare 邏輯(1969): Hoare 在論文 An Axiomatic Basis for Computer Programming 提出 Hoare 邏輯(公理語義學):

\[ \{P\}\; C\; \{Q\}\quad\text{(前置條件 }P\text{,執行程式 }C\text{ 後,保證後置條件 }Q\text{)} \]

這是程式正確性證明與形式化驗證的基石。 - CSP 通訊順序程序(1978): Hoare 發表《Communicating Sequential Processes》—— 並行程式的訊息傳遞模型,直接影響 Go 語言(goroutines/channels)。 - 圖靈獎(1980)與逝世(2025): 1980 年,Hoare 因「程式語言的定義與設計」獲圖靈獎—— 得獎演講《The Emperor's Old Clothes》 批評複雜系統,主張簡單性。 他晚年關注形式化驗證,2025 年 3 月逝世,享年 91 歲。

\[ \text{排序(1960)} \xrightarrow{} \text{null(1965)} \xrightarrow{} \text{Hoare 邏輯(1969)} \xrightarrow{} \text{CSP(1978)} \xrightarrow{} \text{圖靈獎(1980)}. \]
  • 歷史定位:Hoare 是「簡潔的力量」的化身—— 他證明了一個好的演算法,可以只用幾行、 不用額外記憶體、快過一切同類; 而它誕生於一個最平凡的場景:查字典前,先把單字排好。

關鍵人物與文獻#

  • C. A. R. Hoare:Quicksort(1960–1961,發表於 Computer Journal);null reference(1965,自稱「十億美元的錯誤」);An Axiomatic Basis for Computer Programming(1969,Hoare 邏輯);Communicating Sequential Processes(1978,CSP);圖靈獎(1980,演講 The Emperor's Old Clothes);2025 年逝世。
  • E. W. Dijkstra:最短路徑(1959)——分治思想的同代人;後續的「結構化程式設計」與 Hoare 呼應。
  • J. Backus:FORTRAN(1957)——高階語言時代的開端,編譯器內的排序需求浮現。
  • J. von Neumann:merge sort(1945)——quicksort 的對照組(穩定但耗記憶體)。
  • D. Knuth:The Art of Computer Programming Vol. 3(1972)——quicksort 的完整數學分析。
  • B. Kernighan & D. Ritchie:The C Programming Language(1978)——以 quicksort 為遞迴範例,qsort 進入 C 標準。
  • 相關案件:1959-迪傑斯特拉最短路徑.md、1968-克努特演算法藝術.md、1973-UNIX作業系統.md、1957-巴克斯FORTRAN.md。