Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

排序與搜尋

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

線性與二分搜尋、五大經典排序、穩定性,以及為何比較排序逃不過 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 共讀助教正在陪你讀:排序與搜尋:從字典查單字看演算法效率
嗨!我是這篇文章的共讀助教,只根據〈排序與搜尋:從字典查單字看演算法效率〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。