Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

4

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 26 · 臺南 AQI 21 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

排序與搜尋

排序與搜尋:從字典查單字看演算法效率

線性與二分搜尋、五大經典排序、穩定性,以及為何比較排序逃不過 O(n log n) 下限

為什麼字典查單字這麼快,翻通訊錄找人卻這麼慢?

想像你拿到一本沒有編排順序的紙本通訊錄,要從三百個名字裡找出「林宗翰」。你別無選擇,只能從第一頁一個一個往下看,最糟的情況是翻到最後一筆才找到。但同樣三百個單字,你查英漢字典時卻幾乎不費力——因為字典是「排好序」的,你可以翻到中間、判斷目標在前半還是後半,瞬間砍掉一半的範圍。

這個對比點出了計算機科學裡兩個最基礎、也最深刻的問題:搜尋(searching)排序(sorting)。它們看似平凡,卻是幾乎所有系統底層每秒都在做的事——資料庫查詢、搜尋引擎、推薦系統、編譯器,無一例外。更重要的是,這兩個問題是理解「演算法效率」與「問題本質難度」最好的入門教材。

演算法與資料結構概念示意圖

搜尋:線性 vs 二分

最直覺的搜尋是 線性搜尋(linear search):從頭到尾逐一比對,直到找到目標或走完整個序列。它不要求資料事先排序,程式碼也最簡單。

def linear_search(arr, target):
    for i in range(len(arr)):
        if arr[i] == target:
            return i  # 回傳索引
    return -1  # 找不到

在長度為 $n$ 的序列中,線性搜尋平均要比較約 $n/2$ 次,最壞情況比較 $n$ 次,因此時間複雜度是 $O(n)$。

但如果資料已經排序,我們就能用通訊錄查字典的策略——二分搜尋(binary search)。每次都看中間的元素:若中間值等於目標就結束;若目標較小就只在左半邊找,較大就只在右半邊找。每比較一次,搜尋範圍就少一半。

def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            lo = mid + 1   # 目標在右半
        else:
            hi = mid - 1   # 目標在左半
    return -1

因為每一步都把問題規模對半砍,最多砍 $\log_2 n$ 次就會收斂,所以二分搜尋是 $O(\log n)$。$n = 10^9$(十億)筆資料,線性搜尋最壞要比十億次,二分搜尋卻只要約 30 次。這巨大的差距,正是「先排序、再快速搜尋」這種策略值得投資的原因。

常見迷思:二分搜尋並非永遠比線性搜尋好。如果只查一次、而且資料沒排序,那麼為了二分而先排序(至少 $O(n \log n)$)反而比直接線性掃描($O(n)$)更慢。二分搜尋的價值,在於資料會被反覆查詢多次,排序成本得以攤提。

排序的多種策略

既然排序能讓搜尋變快,那麼如何高效地把一堆資料排好?這裡介紹五種經典的比較排序(comparison sort)——它們只透過「兩兩比較大小」來決定順序。

簡單但較慢的三種:$O(n^2)$

氣泡排序(bubble sort):反覆走訪序列,相鄰兩個若順序錯誤就交換,像氣泡一樣讓大的元素逐漸「浮」到最後。

選擇排序(selection sort):每一輪從未排序部分挑出最小值,放到已排序部分的尾端。

插入排序(insertion sort):像整理手中的撲克牌,每次拿一張新牌插入到前面已排好的牌中正確的位置。

這三者最壞情況都是 $O(n^2)$,因為大致都需要兩層迴圈。但它們各有特性:插入排序在資料幾乎已排序時表現極佳,可達接近 $O(n)$;選擇排序的交換次數最少。

動手看一個例子

以插入排序處理序列 [5, 2, 4, 1],逐步觀察「已排序區」如何逐漸長大:

初始:       [5 | 2, 4, 1]    (| 左邊是已排序區)
取 2 插入:   [2, 5 | 4, 1]    2 移到 5 前面
取 4 插入:   [2, 4, 5 | 1]    4 插在 2 和 5 之間
取 1 插入:   [1, 2, 4, 5 |]   1 移到最前面,完成

每一步都維持「左側已排序」這個不變條件(invariant),這正是插入排序好懂又好證明正確性的關鍵。

高效的兩種:$O(n \log n)$

合併排序(merge sort)快速排序(quicksort) 都採用 分治法(divide and conquer):把大問題切成小問題、各自解決、再合併。它們的平均時間複雜度是 $O(n \log n)$,在大資料上遠勝 $O(n^2)$ 的方法,我們將在深入段詳細分析。

穩定性:一個常被忽略的性質

排序演算法還有一個重要性質:穩定性(stability)。若兩個元素的鍵值(key)相同,排序後它們的相對前後順序是否保持不變,若不變則稱此演算法是穩定的。

舉例:一份學生名單已按「姓名」排好,現在要再按「班級」排序。若排序演算法穩定,同一班的學生之間仍會維持原本的姓名順序;若不穩定,同班學生的順序可能被打亂。這在做多鍵排序(先排次要鍵、再排主要鍵)時至關重要。

演算法 平均複雜度 最壞複雜度 額外空間 穩定性
氣泡排序 $O(n^2)$ $O(n^2)$ $O(1)$ 穩定
選擇排序 $O(n^2)$ $O(n^2)$ $O(1)$ 不穩定
插入排序 $O(n^2)$ $O(n^2)$ $O(1)$ 穩定
合併排序 $O(n \log n)$ $O(n \log n)$ $O(n)$ 穩定
快速排序 $O(n \log n)$ $O(n^2)$ $O(\log n)$ 不穩定

從表中可看出沒有「完美」的排序:合併排序穩定且最壞情況也保證 $O(n \log n)$,但需要 $O(n)$ 額外空間;快速排序通常最快、原地排序省空間,但最壞情況退化到 $O(n^2)$ 且不穩定。選擇演算法是一種權衡(trade-off),沒有銀彈。

為什麼比較排序的下限是 $O(n \log n)$?

這是本主題最優雅的理論結果之一。我們能證明:任何只靠「兩兩比較」來決定順序的排序演算法,最壞情況下至少需要 $\Omega(n \log n)$ 次比較——這是物理極限,不是演算法寫得不夠好。

直覺如下:對 $n$ 個元素而言,可能的排列方式共有 $n!$ 種,正確答案是其中唯一一種。每做一次比較,最多只能得到「是」或「否」兩種結果,相當於把可能性最多砍一半。要從 $n!$ 種可能精準鎖定一種,至少需要:

$$\log_2(n!) \text{ 次比較}$$

而根據 Stirling 近似,$\log_2(n!) \approx n \log_2 n - 1.44 n = \Theta(n \log n)$。因此任何比較排序都逃不過 $\Omega(n \log n)$ 的下限。合併排序剛好達到這個下限,是漸近最優(asymptotically optimal) 的比較排序。

這個證明用的是決策樹(decision tree) 模型:把演算法的所有比較分支畫成一棵二元樹,葉子是 $n!$ 種排列,樹高就是最壞比較次數,而有 $n!$ 個葉子的二元樹高度至少 $\log_2(n!)$。

重點回顧

  • 搜尋:未排序資料只能線性搜尋 $O(n)$;已排序資料可用二分搜尋 $O(\log n)$,但前提是承受一次性的排序成本,靠多次查詢攤提。
  • 簡單排序(氣泡、選擇、插入)都是 $O(n^2)$,適合小資料或近乎排序好的資料;插入排序在近乎有序時接近 $O(n)$。
  • 高效排序(合併、快速)採分治法達 $O(n \log n)$;合併排序穩定且最壞情況有保證,快速排序平均最快但最壞退化為 $O(n^2)$。
  • 穩定性決定相同鍵值元素的相對順序是否保留,是多鍵排序的關鍵考量。
  • 比較排序的理論下限是 $\Omega(n \log n)$,由 $\log_2(n!)$ 的決策樹論證得出;要更快只能放棄「純比較」這個前提。

深入探討(研究所視角)

分治法的遞迴分析:主定理

合併排序的時間複雜度可寫成遞迴式:把規模 $n$ 的問題拆成兩個 $n/2$ 子問題(各自遞迴排序),再用 $O(n)$ 的時間線性合併兩個已排序子序列:

$$T(n) = 2\,T(n/2) + O(n)$$

套用 主定理(Master Theorem):此式形如 $T(n) = a\,T(n/b) + f(n)$,其中 $a = 2$、$b = 2$,故 $n^{\log_b a} = n^{\log_2 2} = n$。由於合併成本 $f(n) = \Theta(n)$ 恰好與 $n^{\log_b a}$ 同階(落在第二種情形),得 $T(n) = \Theta(n \log n)$。直覺上,遞迴樹共有 $\log_2 n$ 層,每層所有合併工作加總都是 $O(n)$,相乘即 $n \log n$。

快速排序的分析更微妙。它先選一個 樞紐(pivot),把序列分成「小於樞紐」與「大於樞紐」兩塊(這步稱 partition,$O(n)$),再對兩塊遞迴。若每次都剛好對半分,遞迴式同樣是 $T(n) = 2T(n/2) + O(n) = \Theta(n \log n)$。但若樞紐選得極差(例如序列已排序、又每次都選最小值當樞紐),就會分成 $0$ 與 $n-1$,退化為:

$$T(n) = T(n-1) + O(n) = \Theta(n^2)$$

實務上用 隨機化選樞紐(randomized pivot) 或「三數取中(median-of-three)」來避免最壞情況;可證明隨機化快速排序的期望比較次數為 $\Theta(n \log n)$,且最壞情況發生的機率極低。快速排序之所以在實務上常勝過合併排序,是因為它原地(in-place) 操作、快取局部性(cache locality)好、常數因子小。許多標準函式庫(如 C++ STL)採用 introsort:先跑快速排序,一旦遞迴深度過深就切換到堆積排序(heapsort)以保證最壞 $O(n \log n)$。

突破下限:非比較排序

$\Omega(n \log n)$ 的下限只適用於比較排序。如果我們不靠比較、而是利用鍵值本身的結構,就能突破這條線。

計數排序(counting sort):當鍵值是範圍 $[0, k)$ 的整數時,開一個大小 $k$ 的陣列直接統計每個值出現幾次,再依序輸出。時間複雜度 $O(n + k)$,當 $k = O(n)$ 時即為線性 $O(n)$。它穩定,但只適用於小範圍整數鍵。

基數排序(radix sort):把整數(或字串)依「位數」由低位到高位、反覆用穩定的計數排序處理。處理 $d$ 位數、每位範圍 $k$ 的資料,複雜度為 $O(d \cdot (n + k))$。處理固定長度的整數鍵時近似線性。

桶排序(bucket sort):當資料均勻分布在某區間,把元素灑進若干個桶、各桶內部排序、再串接,期望複雜度 $O(n)$。

這些方法不違反下限定理——它們根本不在「比較排序」的框架內,而是用了關於資料分布或鍵值結構的額外假設換取速度。這呼應了計算理論的核心觀念:問題的難度取決於你被允許使用什麼運算

與其他主題的連結

  • 資料結構:二分搜尋的概念延伸成 二元搜尋樹(BST)平衡樹(AVL、紅黑樹),把 $O(\log n)$ 查詢從靜態陣列推廣到可動態插入刪除的結構;而 雜湊表(hash table) 則用空間換取期望 $O(1)$ 的查詢,是另一種突破比較框架的思路。
  • 外部排序(external sorting):當資料量大到無法塞進記憶體,合併排序的「合併」步驟成為主角——多路歸併(k-way merge) 是大型資料庫與 MapReduce 排序的基礎,因為它只需循序讀取磁碟、I/O 效率高。
  • 下限證明的方法論:決策樹論證是計算複雜度理論中「資訊論下限(information-theoretic lower bound)」的典範,與密碼學、資訊理論中「想唯一確定一個答案至少需要多少位元」是同一套思維。

最後一點關於合法用途與嚴謹性:排序與搜尋演算法本身是中性的工具,但在處理個人資料(如學習歷程、生理數據)時,無論演算法多高效,都應在合法授權與隱私保護的前提下進行。效率的追求,永遠不該凌駕於資料倫理之上。

AI 共讀助教正在陪你讀:排序與搜尋:從字典查單字看演算法效率
嗨!我是這篇文章的共讀助教,只根據〈排序與搜尋:從字典查單字看演算法效率〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。