選修單元 C ECCH6:檢索與排序

本頁以線性檢索對分檢索作為「如何找資料」的代表,再以五種常見排序法(選擇排序法插入排序法冒泡排序法合併排序法快速排序法)建立你對「如何整理資料」的完整框架。 建議學習順序:先掌握概念與流程 → 再看偽代碼 → 最後用Python親手跑一次並理解輸出。

導讀學習地圖:先「排序」再「檢索」?還是相反?

1) 你要解決的兩類問題

  • 檢索(search):在一堆資料中,找出「目標值」是否存在、在甚麼位置。
  • 排序(sort):把資料按某個規則(例如由小至大)重新排列,令資料更易閱讀或更易處理。
很多時候,排序不是「為了好看」,而是為了讓之後的對分檢索或其他處理更有效率。

2) 為何要學不同方法?

不同方法在「時間」與「記憶體」的取捨不同:

  • 有些方法易理解但較慢(例如冒泡排序法)。
  • 有些方法較快但概念較深(例如合併排序法、快速排序法)。
  • 有些方法對資料狀態很敏感(例如插入排序法對「幾乎已排序」的資料特別快)。

3) 本頁你需要帶走的能力

  • 能用自己的話清楚描述每一種方法的步驟(不是只背名字)。
  • 能判斷在某個情境下,哪一種方法較合適(例如資料是否已排序、資料量大小)。
  • 能看懂偽代碼,並用 Python 寫出基本版本。
核心提醒:本頁以陣列作為主要資料結構示例(Python 以 list 示範)。因為陣列支援「按索引直接存取」,最能清楚展示檢索與排序的流程。
小提醒:while / do while 的條件是「繼續循環」;repeat until 的條件是「停止循環」。

甚麼是檢索?

重點

  • 「檢索」的核心任務:在一批資料中找出目標是否存在,以及位置(索引)相關資料
  • 同一個問題可以有不同算法:有些重視「易做、穩陣」,有些重視「大量查找時更快」。
  • 檢索算法的選擇,往往取決於:資料是否已排序、資料量、以及你需要查找的次數。
  • 本課程的核心檢索算法:線性檢索對分檢索;其餘會以增潤形式介紹(已清楚標示)。

在日常生活中,「檢索」其實無處不在:例如在通訊錄中找某位聯絡人、在檔案夾中找某份文件、在名冊中找某位同學。 這些情境的共同點是:你有一堆資料,而你需要在合理時間內找到目標。

在電腦科學中,檢索的關鍵不只是「能不能找到」,而是「需要多少步」。 當資料量很小時,任何方法看起來都差不多;但當資料量變大(例如由 10 增加到 100、1000), 不同算法的差距就會非常明顯。

因此,你學檢索算法時,請特別留意兩件事:第一,算法有沒有前提(例如資料是否已排序); 第二,你是「找一次」還是「找很多次」。 只要你能把這兩點說清楚,你就能在不同題目中選擇合適的方法。

線性檢索(Linear Search)

重點

  • 由陣列第一個元素開始,逐個比較,直到找到目標或到達尾端。
    Python 小練習:印出每次比較(for loop)

    target 改成其他值,再觀察程式會在第幾次比較時停止。

  • 不需要資料已排序;因此在資料無序或資料量不大時非常常用。
    Python 小練習:用 while loop 寫線性檢索

    請先理解:即使 A 是無序,線性檢索仍然能找到目標(只是可能要比較較多次)。

  • 最壞情況下需要比較 n 次(n = 元素數量),所以效率會隨資料量增加而明顯下降。
    Python 小練習:驗證「target 不存在」時要比較 n 次

    A 改長一點(例如 1..100),再看比較次數是否等於 len(A)

線性檢索之所以容易理解,是因為它模仿了人類「逐個檢查」的習慣:例如在一列名字中找某位同學,你會從第一個名字開始向下掃,直到看到目標名字才停下來。 在程式中,這種做法最直接,亦最不容易出錯。

但線性檢索的代價也很明顯:如果資料很多,而目標又經常出現在後面,或根本不存在,程式就需要做大量無效比較。 因此,當資料量變大,或者你需要重複檢索很多次時,便要考慮是否值得先把資料排序,再使用更有效率的對分檢索。

在學習檢索時,你不只要記住步驟,更要培養「比較次數感」: 當 n 變成 10 倍,線性檢索最壞情況比較次數也會接近 10 倍增加,這就是時間複雜度帶來的直觀影響。

互動示範:線性檢索(可自行生成資料)

你可以自行設定「數據項目數量」及「數值範圍」,按「生成資料」後,再用「下一步」觀察每次比較的位置(當前索引會被標記)。

圖像化例子:線性檢索的逐步比較

目標:在陣列 [7, 2, 9, 4, 2, 5, 1, 8, 6, 3] 中尋找 4

步驟iA[i]比較結果累計比較次數
1077 ≠ 41
2122 ≠ 42
3299 ≠ 43
434命中4
索引:  0   1   2   3   4
數值: [7] [2] [9] [4] [2]
指針:  ^   →   →   ^(找到) 

示例:偽代碼 ↔ Python(4 種偽代碼循環 + 2 種 Python 寫法)

提示:可在 Python 分頁按「Run」觀察輸出;偽代碼分頁用於對照 DSE 風格。

線性檢索(A, target)
設 n = 長度(A)
設 i 由 0至 n-1
    如果 A[i] = target
        傳回 i
傳回 -1

Check Point:小測/小遊戲(線性檢索)

(請按「下一題」開始)
得分:0 / 0

對分檢索(Binary Search)

重點

  • 只適用於已排序陣列;若資料無序,必須先排序,否則結果不可靠。
    Python 小練習:比較「未排序」與「已排序」的對分檢索結果

    先直接跑一次,再把 target 改成其他值,觀察未排序時為何會出現錯誤結果。

  • 每次比較中間元素,然後把搜尋範圍縮小為一半(排除另一半)。
    Python 小練習:印出 low / high / mid(while loop 追蹤)

    這段程式會逐步印出指標變化。你可以把 target 改成不存在的值,看看最後如何退出循環。

  • 時間複雜度約為 O(log n),資料量越大,優勢越明顯。
    Python 小練習:估算最壞情況需要幾多步(while loop)

    以下模擬「一直向右縮」的最壞情況。嘗試把 n 改成 2048、4096… 觀察步數如何增加。

對分檢索的核心價值在於「把問題規模一分為二」:每一次比較之後,都能確定目標只可能存在於其中一半。 但要做到這點,前提是陣列必須已排序,否則你無法確定「目標應該在左還是右」。

從學習角度而言,你應把 lowhighmid 視為三個角色: mid 負責「做判斷」,而 low/high 負責「縮小範圍」。 只要你能確保每次迴圈後範圍都縮小,對分檢索就一定會結束。

在實際應用中,對分檢索常常與排序一起出現:先用排序法把資料整理好,之後便可多次使用對分檢索快速查找。 因此,理解排序與檢索的關係,是建立「效率思維」的重要一步。

互動示範:對分檢索(可自行生成資料)

對分檢索需要「已排序」資料。系統會自動把生成的資料排序,並用標記顯示 low / mid / high 的變化。

🎮 小遊戲:你就係 Binary Search 程式

圖像化例子:對分檢索的 low / mid / high

目標:在已排序陣列 [1, 3, 4, 6, 8, 9, 11, 15] 中尋找 9

步驟lowhighmidA[mid]下一步
10736目標較大 → low = mid + 1 = 4
24759命中
索引:  0   1   2   3   4   5   6   7
數值: [1] [3] [4] [6] [8] [9] [11][15]
              ^mid=3           → low=4
                  low=4   ^mid=5 (找到) 

示例:偽代碼 ↔ Python(4 種偽代碼循環 + 2 種 Python 寫法)

提示:可在 Python 分頁按「Run」觀察輸出;偽代碼分頁用於對照 DSE 風格。

對分檢索(A, target)
設 low = 0
設 high = 長度(A) - 1
設 step 由 1至 長度(A)      // 最多做 n 次(實際通常更少)
    如果 low > high
        跳出循環
    設 mid = (low + high) 整除 2
    如果 A[mid] = target
        傳回 mid
    否則如果 A[mid] < target
        設 low = mid + 1
    否則
        設 high = mid - 1
傳回 -1

Check Point:小測/小遊戲(對分檢索)

(請按「下一題」開始)
得分:0 / 0

增潤其他檢索算法(增潤部份)

增潤部分:點擊展開/收合(雜湊檢索、插值檢索)

以下內容屬增潤部分。建議你先完成「線性檢索」與「對分檢索」,再按需要展開閱讀及挑戰小遊戲。

雜湊檢索(Hash-based Search)﹙增潤部份﹚

重點

  • 雜湊檢索會把「鍵值」透過雜湊函數映射到桶(bucket),平均情況下查找可接近 O(1)
    Python 小練習:用 set 做「是否存在」查找
  • 碰撞(collision)是雜湊檢索的核心挑戰:不同鍵值可能映射到同一個桶,需要額外策略處理(例如鏈結法、開放定址法)。
    Python 小練習:觀察 key % m 造成的碰撞
  • 開放定址法(open addressing)其中一種做法是線性探測:桶位已滿就向後逐格查找下一個空位;查找時也要用相同方式探測。
    Python 小練習:完成線性探測查找函數(while loop)
  • 與對分檢索相比:雜湊檢索通常不需要先排序,但不擅長「範圍查詢」(例如找出介乎 20 至 40 的所有值)。
    Python 小練習:比較「set 查找」與「範圍查找」的差異

雜湊檢索的核心思想是:不要逐個比對(線性),也不要每次取中位(對分),而是用雜湊函數把「查找」變成「直接定位到某個桶位」。

在程式實務中,Python 的 setdict 已經封裝了雜湊表與碰撞處理,因此通常是最快速、最可靠的做法之一。

不過,在 DSE 或演算法理解層面,仍需要掌握碰撞的原因與處理方法,才能解釋為何雜湊檢索不是「無條件」O(1)。

互動示範:雜湊檢索(100 個數據,10×10 顯示)

你可以生成 100 個數字,系統會以 value mod 10 作為簡化雜湊函數,把資料分到 10 個桶(bucket)。 你亦可以自行輸入目標值,觀察「先定位桶」再「在桶內查找」的流程。

🎮 小遊戲:Hash Search(鏈結法 / 線性試探)

圖像化:桶(bucket)與碰撞

m=7h(key)=key%7 為例:

桶位: 0   1   2   3   4   5   6
      [ ] [ ] [ ] [ ] [ ] [ ] [ ]
插入 10 → 10%7=3 → 放入桶 3
插入 17 → 17%7=3 → 桶 3 已滿(碰撞)→ 線性探測:改放桶 4
插入 24 → 24%7=3 → 桶 3/4 已滿 → 改放桶 5
keyh(key)碰撞?最終放置
1033
1734
2435

程式示例:不同版本(可切換)

先用「版本 A」理解實務用法,再用「版本 B」理解碰撞處理的底層原理。

做法:建立 m 個桶(陣列),每個桶是一個列表(list)。碰撞時,把多個 key 放在同一桶內。

  • h(key) = key % m
  • 插入:buckets[h(key)].append(key)
  • 搜尋:先到 buckets[h(target)],再線性檢查該桶
HashSearch_Chaining(A, target, m)
buckets = array of m empty lists
for each x in A:
    buckets[x % m].append(x)

b = target % m
for each y in buckets[b]:
    if y == target:
        return FOUND
return NOT_FOUND

這個版本展示「最常用的實務做法」:用 dict / set 以平均 O(1) 速度查找。

優點:簡潔、可靠;缺點:原理被封裝,需要理解碰撞與負載因子等概念。

雜湊檢索_建立索引(A)
設 H = 空雜湊表
設 i 由 0 至 長度(A) - 1
    如果 H 不包含鍵 A[i] 則
        H[A[i]] = i    // 記錄第一次出現的位置
傳回 H

雜湊檢索(A, target)
設 H = 雜湊檢索_建立索引(A)
如果 H 包含鍵 target 則
    傳回 H[target]
否則
    傳回 -1

Check Point:雜湊檢索

(載入中…)

插值檢索(Interpolation Search)﹙增潤部份﹚

重點

  • 插值檢索要求資料 已排序,並假設數值分佈相對均勻,才有機會比對分檢索更快。
    Python 小練習:建立已排序資料並測試查找
  • 核心公式(估算位置): pos = low + (target - A[low]) * (high - low) / (A[high] - A[low])
    Python 小練習:只計算一次 pos(觀察估算位置)
  • 插值檢索最壞情況仍可能退化到 O(n),常見原因包括:分佈極不均勻、或資料大量重複導致估算失真。
    Python 小練習:加入防護避免除以零

插值檢索的直覺是:如果資料值分佈均勻,目標值 90 應該比目標值 10 更接近右邊,因此可以先猜一個「更靠近目標的中間點」來比較。

對分檢索每次都取正中間,保證把搜尋範圍對半縮小;插值檢索改用比例估算位置,可能更快,也可能因估算不準而退化。

互動示範:插值檢索(100 個數據,10×10 顯示)

插值檢索會利用 target 在數值範圍中的相對位置去估算 pos。 系統會自動把資料排序,並標示 low / pos / high 的移動。

🎮 小遊戲:你就係 Interpolation Search 程式

圖像化:pos 估算位置

pos = low + (target - A[low]) * (high - low) / (A[high] - A[low])

A: [1, 3, 4, 6, 8, 9, 11, 15]
    low ------------------- high
                 ^ pos(估算靠近 target)

當資料分佈均勻,pos 的估算通常更接近目標;當分佈不均或重複值多,pos 可能失真。

程式示例:不同版本(可切換)

建議先閱讀版本 A,再閱讀版本 B 的防護條件。

利用比例估算位置 pos,在資料分佈較均勻時可快於對分檢索。

此版本假設 A[low] != A[high],並省略部分防護。

插值檢索(A, target)
設 low = 0
設 high = 長度(A) - 1

設 t 由 1 至 長度(A)
    如果 low > high 則
        傳回 -1
    如果 target < A[low] 或 target > A[high] 則
        傳回 -1

    設 pos = low + (target - A[low]) * (high - low) div (A[high] - A[low])

    如果 A[pos] == target 則
        傳回 pos
    如果 A[pos] < target 則
        low = pos + 1
    否則
        high = pos - 1

傳回 -1

Check Point:插值檢索

(載入中…)

甚麼是排序?

重點

  • 「排序」的核心任務:把資料按指定規則重新排列,例如由小至大由大至小
  • 排序後,很多事情會變得更容易:例如更快檢索(對分檢索)、更容易找最大/最小、以及更清晰的輸出。
  • 不同排序法的差異主要在於:時間複雜度、是否需要額外記憶體、以及是否穩定排序。
  • 本課程的核心排序法:選擇排序法插入排序法冒泡排序法合併排序法快速排序法只需認識其存在與基本原理;其餘會以增潤形式介紹(已清楚標示)。

排序不只是「把數字排好看一點」,而是一種非常常用的前置處理。 很多看似不同的問題(例如找最大值、找第 k 大、快速查找某個值)在排序後都會變得容易。

在學習排序法時,你可以把陣列想像成一排卡牌。不同排序法的差異, 就是「你每次用甚麼策略把卡牌搬到合適的位置」:有的會不斷把最小值搬到前面,有的會把新牌插入到已排好的位置,也有的會用 pivot 把牌分成兩邊再處理。

另外,排序題目常見的得分位在於「追蹤過程」:你能否清楚指出每一步比較了哪兩個元素、是否交換,陣列如何改變。 本頁面的互動示範會用標記顯示當前比較的位置,幫你建立正確的追蹤習慣。

選擇排序法(Selection Sort)

重點

  • 每一輪在「未排序區」中找出最小值,放到「已排序區」的尾端。
    Python 小練習:做第 1 輪(找最小值並交換)

    先只做 i = 0 這一輪。然後把 i 改成 1,再做下一輪。

  • 時間複雜度一般為 O(n²);即使資料已接近排序,仍然要做大量比較。
    Python 小練習:計算比較次數(for loop)

    選擇排序法的比較次數與資料內容無關,只與 n 有關。試試改 n

  • 一般情況下屬於不穩定排序:相同鍵值的元素,排序後相對次序可能改變。
    Python 小練習:用「標籤」觀察不穩定

    留意:(2, "A")(2, "B") 的相對次序會在交換後改變。

選擇排序法的思路很像「選拔」:把最小值找出來,放到第一位;再把剩下的最小值找出來,放到第二位。 由於流程固定、變數少,它特別適合用來練習「找最小值」與「交換」這兩個常見技巧。

不過,你要留意它的效率限制:即使資料本身已經差不多排序好,選擇排序法仍然會照樣把每一輪的搜尋做完。 這意味著它的比較次數通常仍接近 ,在資料量大時會明顯變慢。

學習選擇排序法時,建議你同時建立「已排序/未排序」的視覺:左邊區域每輪增加一格,右邊區域逐步縮小。 只要能清楚說出「第 i 輪後,前 i 個元素已經在正確位置」,你就真正理解了它的結構。

互動示範:選擇排序法(可自行生成資料)

觀察每一輪如何在「未排序區」找出最小值,並交換到前面。系統會標記「目前比較」與「目前最小值」的位置。

🎮 小遊戲:扮演選擇排序法(簡易版 / 空運行版)

圖像化例子:選擇排序法(每一輪選最小)

示例陣列:[5, 1, 4, 2]

輪次 i未排序範圍找到的最小值交換後結果
0[5, 1, 4, 2]1[1, 5, 4, 2]
1[5, 4, 2]2[1, 2, 4, 5]
2[4, 5]4[1, 2, 4, 5]

提示:即使陣列幾乎已排序,選擇排序法仍會完整掃描未排序範圍來找最小值。

示例:偽代碼 ↔ Python(4 種偽代碼循環 + 2 種 Python 寫法)

提示:可在 Python 分頁按「Run」觀察輸出;偽代碼分頁用於對照 DSE 風格。

選擇排序法(A)
設 n = 長度(A)
設 i 由 0至 n-2
    設 minIndex = i
    設 j 由 i+1至 n-1
        如果 A[j] < A[minIndex]
            設 minIndex = j
    如果 minIndex ≠ i
        設 temp = A[i]
        設 A[i] = A[minIndex]
        設 A[minIndex] = temp

Check Point:小測/小遊戲(選擇排序法)

(請按「下一題」開始)
得分:0 / 0

插入排序法(Insertion Sort)

重點

  • 維持「左邊已排序、右邊未排序」:每次把一個元素插入到已排序區的正確位置。
    Python 小練習:把 key 插入已排序區(while loop)

    這段程式示範「搬移」與「插入」的核心動作,是理解插入排序法的關鍵。

  • 對「幾乎已排序」的資料特別有效;搬移次數很少,速度可以很快。
    Python 小練習:比較「幾乎已排序」與「完全倒序」的搬移次數

    把陣列改一改,再觀察 shifts(搬移次數)如何大幅改變。

  • 最佳情況約為 O(n)(已排序),最壞情況約為 O(n²)(倒序)。
    Python 小練習:同一個 n,最佳與最壞搬移次數差幾多?

    下面用相同 n 比較「已排序」與「倒序」的搬移次數。

插入排序法的特點在於它不急於把整個陣列一次過整理好,而是把「已排序」部分逐步擴大。 每次你只處理一個新元素,把它放回正確位置,便令左邊區域再次保持有序。

它的效率很依賴資料狀態:若資料完全倒序,每次插入都要把整段已排序部分向右搬,會造成大量搬移與比較,速度就會變慢; 但若資料本來就差不多排好,大部分元素只需搬移一兩格,便能迅速完成。

因此,在面對「資料常常只做小幅更新」的情境(例如新增一個分數到已排序名單),插入排序法的思路非常實用: 你不用重新排序整份名單,只要把新元素插入到正確位置即可。

互動示範:插入排序法(可自行生成資料)

觀察 key 如何從右邊取出,並向左插入到「已排序區」的正確位置;被移動的元素會被標記。

🎮 小遊戲:扮演插入排序法(每輪拖 key 一次)

圖像化例子:插入排序法(把 key 插入左邊的已排序區)

示例陣列:[5, 1, 4, 2]

ikey左邊已排序區插入後結果
11[5][1, 5, 4, 2]
24[1, 5][1, 4, 5, 2]
32[1, 4, 5][1, 2, 4, 5]

提示:若原本已接近排序,插入排序法的移動次數會明顯減少。

示例:偽代碼 ↔ Python(4 種偽代碼循環 + 2 種 Python 寫法)

提示:可在 Python 分頁按「Run」觀察輸出;偽代碼分頁用於對照 DSE 風格。

插入排序法(A)
設 n = 長度(A)
設 i 由 1至 n-1
    設 key = A[i]
    設 j = i - 1
    當 j >= 0 並且 A[j] > key
        設 A[j+1] = A[j]
        設 j = j - 1
    設 A[j+1] = key

Check Point:小測/小遊戲(插入排序法)

(請按「下一題」開始)
得分:0 / 0

冒泡排序法(Bubble Sort)

重點

  • 反覆比較相鄰元素並在需要時交換;每一輪會把一個「最大值」推到尾端。
    Python 小練習:做第 1 輪(觀察最大值如何被推到尾端)

    留意每一步的陣列變化:較大的值會一步一步「冒」到右邊。

  • 多輪重複以上過程,直到整個陣列已排序;時間複雜度一般為 O(n²)
    Python 小練習:每一輪後印出陣列(for loop)

    這能「圖像化」冒泡排序法的過程:越大的值越早固定在右邊。

  • 可加入「早停」:若某一輪沒有交換,代表已排序完成,可提前停止。
    Python 小練習:比較「早停」能節省幾多輪

    已排序的陣列用早停會非常快;試試把輸入改成「幾乎已排序」。

冒泡排序法可以看成「不斷把錯位的相鄰元素調回正確次序」。 它最大的教學價值,是令你看見排序的本質:排序其實就是透過比較,逐步消除「逆序」的情況。

當你理解了「每一輪會把最大值放到最右」這件事,你便能自然推導出內迴圈的範圍為何可以逐步縮短, 以及為何早停優化在「已接近排序」的資料中會特別有效。

不過,冒泡排序法的效率限制同樣值得記住:當資料量很大時,兩層迴圈會帶來大量比較與交換。 因此,在需要處理大量資料時,冒泡排序法通常只適合作為概念示範,而不是首選方法。

互動示範:冒泡排序法(可自行生成資料)

觀察相鄰比較與交換:較大的元素會逐步「冒」到右端。系統會標記正在比較的一對元素。

🎮 小遊戲:Bubble Sort(比較相鄰一對)

圖像化例子:冒泡排序法(把最大值「冒」到右端)

示例陣列:[5, 1, 4, 2]

輪次相鄰比較與交換輪次結束後
第 1 輪(5,1)→換;(5,4)→換;(5,2)→換[1, 4, 2, 5]
第 2 輪(1,4)→不換;(4,2)→換[1, 2, 4, 5]

加入「若某一輪沒有交換就提前停止」的優化,可令近乎排序的情況更快。

示例:偽代碼 ↔ Python(4 種偽代碼循環 + 2 種 Python 寫法)

提示:可在 Python 分頁按「Run」觀察輸出;偽代碼分頁用於對照 DSE 風格。

冒泡排序法(A)
設 n = 長度(A)
設 pass 由 0至 n-2
    設 i 由 0至 n-2-pass
        如果 A[i] > A[i+1]
            設 temp = A[i]
            設 A[i] = A[i+1]
            設 A[i+1] = temp

Check Point:小測/小遊戲(冒泡排序法)

(請按「下一題」開始)
得分:0 / 0

合併兩個已排序的列表(Merge Two Sorted Lists)

重點

  • 使用兩個指針 ij,分別指向兩個列表當前尚未合併的元素。
    Python 小練習:手動追蹤 i / j 的移動
  • 每次比較 A[i]B[j],把較小者放入輸出,然後移動相應指針(穩定性:相等時優先取左邊可保持穩定)。
    Python 小練習:相等時先取 A(維持穩定)
  • 時間複雜度為 O(n+m),因為每個元素最多被放入輸出一次;這正是合併排序法能達到 O(n log n) 的關鍵子步驟。
    Python 小練習:確認合併次數

合併排序法(Merge Sort)之所以高效,是因為它把問題分成兩半後,最後用「線性時間」把兩個已排序子列表合併起來。

因此,在學合併排序法之前,必須先熟悉這個合併子程序:雙指針、逐步比較、把較小者放入輸出。

互動示範:合併兩個已排序列表

系統會生成兩個已排序的列表(A 與 B),並用指針 ij 逐步合併到新列表。 你可以調整每個列表的長度與數值範圍,然後按「下一步」觀察指針如何前進。

🎮 小遊戲:Merge Two Sorted Lists

圖像化:i / j 指針的移動

A = [1, 4, 7, 8]
     i^
B = [2, 3, 9]
     j^

比較 A[i]=1 與 B[j]=2 → 取 1,i++
再比較 A[i]=4 與 B[j]=2 → 取 2,j++
...

程式示例:偽代碼與 Python(可切換)

此合併子程序會在合併排序法中反覆使用。

合併兩個已排序列表(A, B)
設 i = 0
設 j = 0
設 output = 空列表

設 k 由 1 至 長度(A) + 長度(B)
    如果 i == 長度(A) 則
        將 B[j] 加入 output
        j = j + 1
    否則 如果 j == 長度(B) 則
        將 A[i] 加入 output
        i = i + 1
    否則 如果 A[i] <= B[j] 則
        將 A[i] 加入 output
        i = i + 1
    否則
        將 B[j] 加入 output
        j = j + 1

傳回 output

Check Point:合併兩個已排序列表

(載入中…)

增潤其他排序算法(增潤部份)

增潤部分:點擊展開/收合(合併排序法、快速排序法、計數排序法、基數排序法)

以下內容屬增潤部分。建議你先完成「選擇排序法/插入排序法/冒泡排序法」等核心排序法,再按需要展開閱讀及挑戰小遊戲。

合併排序法(Merge Sort)﹙增潤部份﹚

重點

  • 採用「分治」:先把陣列分成兩半,分到最細,再把兩個已排序子陣列合併
    Python 小練習:實作 merge(用 while loop 合併兩個已排序 list)

    合併是合併排序法的核心。請先確定 merge() 能正確合併,再延伸到完整排序。

  • 時間複雜度一般為 O(n log n),表現穩定;資料是否接近排序影響不大。
    Python 小練習:計算分割層數(大約等於 log₂(n))

    每一層把資料量減半,直到每份只剩 1 個元素。

  • 通常需要額外 O(n) 記憶體(合併時建立新陣列),換取穩定且可預期的效率。
    Python 小練習:觀察「傳回新 list」與「原 list 不變」

    合併排序法常以「傳回新陣列」方式實作,因此會用到額外空間。

合併排序法是你正式接觸「分而治之」思維的重要起點:先把大問題拆小,解決小問題後再把答案合併成完整解。 你不必一開始就能背出所有細節,但你必須能說清楚「拆分」與「合併」各自的目的。

對很多學生而言,最難的是遞歸:你會覺得程式好像在「自己叫自己」。 其實你只需要牢記兩點:第一,必須有停止條件;第二,每一次遞歸都令問題變小(例如陣列長度減半)。 只要這兩點成立,遞歸就會自然收斂並返回答案。

合併排序法的另一個關鍵是「合併」本身非常規律:兩個已排序列表,就像兩條已排好隊的隊伍,你只要每次選出較小的一個向前走,最終就能合成一條更長的已排序隊伍。 這種合併過程是線性的,因此整體時間複雜度可以保持在 O(n log n)

互動示範:合併排序法(100 個數據,10×10 顯示)

合併排序法的示範預設使用 100 個數據(10×10 排列)。你可以改變數量與範圍,並選擇排序方向(由小至大/由大至小)。

🎮 小遊戲:Merge Sort(先一半一半分割,再合併)

圖像化例子:合併排序法的「分」與「合」

示例陣列:[8, 4, 7, 3, 1, 9, 2]

分(Divide):
[8, 4, 7, 3, 1, 9, 2]
→ [8, 4, 7, 3] + [1, 9, 2]
→ [8, 4] + [7, 3] + [1, 9] + [2]
→ [8] [4] [7] [3] [1] [9] [2]

合(Conquer / Merge):
[8] + [4] → [4, 8]
[7] + [3] → [3, 7]
[4, 8] + [3, 7] → [3, 4, 7, 8]
[1] + [9] → [1, 9]
[1, 9] + [2] → [1, 2, 9]
[3, 4, 7, 8] + [1, 2, 9] → [1, 2, 3, 4, 7, 8, 9]

關鍵:每一次「合併」都在合併兩個已排序的列表;因此上一節已先獨立講解「合併兩個已排序列表」。

示例:偽代碼 ↔ Python(4 種偽代碼循環 + 2 種 Python 寫法)

提示:可在 Python 分頁按「Run」觀察輸出;偽代碼分頁用於對照 DSE 風格。

合併排序法(A)
如果 長度(A) <= 1
    傳回 A
設 mid = 長度(A) 整除 2
設 left = 合併排序法(A[0..mid-1])
設 right = 合併排序法(A[mid..最後])
傳回 合併_for(left, right)

合併_for(left, right)    // 示範 for loop
設 i = 0
設 j = 0
設 out = 空陣列
設 k 由 1至 長度(left)+長度(right)
    如果 i = 長度(left)
        加入 right[j] 到 out
        設 j = j + 1
    否則如果 j = 長度(right)
        加入 left[i] 到 out
        設 i = i + 1
    否則如果 left[i] <= right[j]
        加入 left[i] 到 out
        設 i = i + 1
    否則
        加入 right[j] 到 out
        設 j = j + 1
傳回 out

Check Point:小測/小遊戲(合併排序法)

(請按「下一題」開始)
得分:0 / 0

快速排序法(Quick Sort)﹙增潤部份﹚

重點

  • 選一個 pivot(樞紐)把資料分成「比 pivot 小 / 等於 / 比 pivot 大」三部分,再遞歸處理兩邊。
    Python 小練習:先做分割(partition)

    先學會分割,你就掌握了快速排序法的核心一步。

  • 平均時間複雜度約為 O(n log n);如果 pivot 分得平均,遞歸深度較淺,通常很快。
    Python 小練習:一次實驗量度遞歸深度(random pivot)

    這只是一次隨機實驗;你可以多跑幾次,或把資料量改大,觀察深度變化。

  • 最壞情況會退化到 O(n²)(例如 pivot 選得很差、資料已排序);因此 pivot 選擇很重要。
    Python 小練習:已排序資料 + 固定用第一個 pivot(示範最壞情況)

    已排序資料若每次都選第一個做 pivot,會令其中一邊幾乎是空,遞歸深度接近 n。

快速排序法的名字之所以叫「快速」,不是因為它每一步都很簡單,而是因為它在平均情況下能很有效地把資料分開, 令每次遞歸處理的範圍都大幅縮小,從而在大多數情況下表現出色。

學習快速排序法時,你最少要掌握「pivot 令資料分成兩邊」這個核心概念。 至於 partition 的細節(不同指標寫法)可以先選一種熟悉的版本練熟,確保你能用追蹤表說明每一步指標如何移動。

此外,你應建立正確的期望:快速排序法並非永遠最快,它存在最壞情況退化。 但由於最壞情況並不常見,加上它常可原地分割、記憶體額外需求較低,因此在許多實務系統中仍然非常常用。

互動示範:快速排序法(100 個數據,10×10 顯示)

快速排序法的示範預設使用 100 個數據(10×10 排列)。你可以切換不同版本(本節下方有三種版本的程式與原理),並用標記觀察 partition 的移動與交換。

🎮 小遊戲:Quick Sort(pivot 三路分割概念)

圖像化例子:快速排序法的分割概念

示例陣列:[8, 4, 7, 3, 1, 9, 2],以 pivot = 7 為例。

一次分割(概念):
left  : [4, 3, 1, 2]   (< pivot)
mid   : [7]            (= pivot)
right : [8, 9]         (> pivot)

之後分別對 left / right 做遞歸排序,再把結果接回一起。
版本是否原地(in-place)分割點特性備註
三區分割否(需額外列表)直接得到 left/mid/right易讀,對重複值友善
Lomuto傳回 pivot 的最終位置程式較直觀;pivot 選得差時易退化
Hoare傳回分割邊界(不一定是 pivot 位置)交換較少;遞歸區間與 Lomuto 不同

示例:偽代碼 ↔ Python(4 種偽代碼循環 + 2 種 Python 寫法)

此處示範三個常見版本:三區分割(left/mid/right)、Lomuto 分割、Hoare 分割。可在不同版本之間比較「pivot 選擇、交換方式、返回的分割點」。

特點:一次掃描把資料分成 left / mid / right 三段,易讀,對重複值較友善。

代價:需要額外列表,空間較高(非原地)。

快速排序法(A)
如果 長度(A) <= 1
    傳回 A
設 pivot = A[0]
設 left = 空陣列
設 equal = 空陣列
設 right = 空陣列
設 i 由 0至 長度(A)-1
    如果 A[i] < pivot
        加入 A[i] 到 left
    否則如果 A[i] > pivot
        加入 A[i] 到 right
    否則
        加入 A[i] 到 equal
傳回 快速排序法(left) + equal + 快速排序法(right)

Check Point:小測/小遊戲(快速排序法)

(請按「下一題」開始)
得分:0 / 0

計數排序法(Counting Sort)﹙增潤部份﹚

重點

  • 計數排序法不是「比較排序」,而是先統計每個值出現的次數,再重建排序結果。
    Python 小練習:建立頻率表
  • 當值域大小為 k 時,時間複雜度約為 O(n + k);如果 k 很大,反而會變慢且佔用記憶體。
    Python 小練習:比較 n 與 k 的大小
  • 若使用「前綴和 + 由後向前放置」,計數排序法可以做到穩定排序(常用於基數排序法的子程序)。
    Python 小練習:觀察穩定性(保持相同 key 的相對次序)
  • 計數排序法適用於離散且範圍不大的整數,例如:分數(0–100)、年齡(0–120)、等級(1–10)。
    Python 小練習:把 0–100 的分數排序

在「值域不大」的情況下,計數排序法往往比 O(n log n) 的比較排序更快,因為它避免了大量元素比較。

它的速度來自兩個線性階段:一次掃描統計、一次掃描重建。但代價是需要一個與值域大小成正比的 count 陣列。

互動示範:計數排序法(100 個數據,10×10 顯示)

計數排序法會先建立「出現次數」的頻率表(count),再重建排序後的輸出。 你可以選擇排序方向,並觀察「數到幾多個」如何變成「排好序的結果」。

🎮 小遊戲:Counting Sort(放入計數桶)

圖像化:頻率表(count)如何重建排序結果

例子:A=[4,2,2,8,3,3,1]k=8

count 索引: 0 1 2 3 4 5 6 7 8
count 次數:  0 1 2 2 1 0 0 0 1

重建輸出:
1 出現 1 次 → [1]
2 出現 2 次 → [1,2,2]
3 出現 2 次 → [1,2,2,3,3]
4 出現 1 次 → [1,2,2,3,3,4]
8 出現 1 次 → [1,2,2,3,3,4,8]

程式示例:不同版本(可切換)

版本 A 著重概念;版本 B 展示穩定性與負數處理。

適用於值域不大(0..k)的整數資料。核心是「用 count 記錄每個值出現次數」。

計數排序(A, k)
# A 內的值介乎 0..k(非負整數)

設 count[0..k] = 0
設 i 由 0 至 長度(A) - 1
    count[A[i]] = count[A[i]] + 1

設 output = 空列表
設 v 由 0 至 k
    設 c 由 1 至 count[v]
        將 v 加入 output

傳回 output

Check Point:計數排序法

(載入中…)

基數排序法(Radix Sort)﹙增潤部份﹚

重點

  • 基數排序法會「逐位(digit)」排序(常見是由低位到高位 LSD),每一輪都用一個穩定排序處理該位。
    Python 小練習:取得某個數的個位 / 十位
  • 若每一位的子排序是穩定的,最終結果才會正確;因此常用「穩定計數排序」作為子程序。
    Python 小練習:思考穩定性的重要性
  • 典型複雜度:若每輪是 O(n+k),總輪數為 d(位數),則整體約為 O(d*(n+k));當 d 不大時非常快。
    Python 小練習:估算位數 d
  • 適合固定長度的整數或字串(例如身份證號碼、日期碼、固定長度代碼),不適合任意精度浮點或超長字串直接使用。
    Python 小練習:把固定長度字串按字元排序(概念)

基數排序法常被視為「非常快」的排序法之一,原因是它把排序拆成多輪較簡單的子排序(例如計數排序),每輪都可以做到線性級別。

若位數 d 不大(例如 32-bit 整數最多 4 個 byte 或 10 進制約 10 位以內),總輪數很少,因此整體速度往往非常理想。

互動示範:基數排序法(100 個數據,10×10 顯示)

基數排序法(LSD)會由個位開始,逐位(十位、百位……)進行穩定排序。 你可以觀察每一輪「按位數分桶」如何逐步得到完全排序的結果。

🎮 小遊戲:Radix Sort(逐位放桶)

圖像化:LSD(由低位到高位)

以 802、170、90 為例(十進制 LSD):
第 1 輪(個位):802(2), 170(0), 90(0) → 先按個位排
第 2 輪(十位):802(0), 170(7), 90(9) → 再按十位排(必須穩定)
第 3 輪(百位):802(8), 170(1), 90(0) → 最後按百位排

程式示例:不同版本(可切換)

版本 A 以 base 10 展示概念;版本 B 以 base 256 展示較底層的思路。

每一輪按「某一位 digit」做一次穩定排序(常用計數排序),由低位到高位。

基數排序_LSD(A)
# 以 10 進制、由低位到高位(LSD)排序

設 maxV = 最大值(A)
設 d = 位數(maxV)
設 exp = 1

設 t 由 1 至 d
    A = 依位計數排序(A, exp)
    exp = exp * 10

傳回 A

依位計數排序(A, exp)
設 count[0..9] = 0
設 i 由 0 至 長度(A) - 1
    設 digit = (A[i] div exp) mod 10
    count[digit] = count[digit] + 1

設 v 由 1 至 9
    count[v] = count[v] + count[v - 1]

設 output[0..長度(A)-1]
設 i 由 長度(A) - 1 至 0
    設 digit = (A[i] div exp) mod 10
    count[digit] = count[digit] - 1
    output[count[digit]] = A[i]

傳回 output

Check Point:基數排序法

(載入中…)

Python 練習:除錯與填空(按算法分組)

重點

  • 每種算法提供 5–6 題「除錯/填空」練習:由 1–2 行開始,逐步增加到 最多約 5 行
  • 練習的目標不是背答案,而是建立習慣:追蹤變量 → 找出錯誤位置 → 修正 → 再測試
  • 大部分題目要求你只輸出指定結果(例如索引或排序後的數列),方便自動批改。

練習題(點開對應算法)

每個「除錯/填空」練習都可以直接按「Run」執行;完成後按「Check」讓系統檢查輸出是否符合要求。 如你想自行挑戰,可先把 print 的例子換成自己新的測試數據。

線性檢索(Linear Search)

除錯/填空(5 題)

自行實作(1 題)

對分檢索(Binary Search)

除錯/填空(5 題)

自行實作(1 題)

雜湊檢索(Hash Search|增潤)

除錯/填空(5 題)

自行實作(1 題)

插值檢索(Interpolation Search|增潤)

除錯/填空(5 題)

自行實作(1 題)

選擇排序法(Selection Sort)

除錯/填空(5 題)

自行實作(1 題)

插入排序法(Insertion Sort)

除錯/填空(5 題)

自行實作(1 題)

冒泡排序法(Bubble Sort)

除錯/填空(5 題)

自行實作(1 題)

合併兩個已排序列表(Merge Two Sorted Lists)

除錯/填空(5 題)

自行實作(1 題)

合併排序法(Merge Sort|只需認識)

除錯/填空(5 題)

自行實作(1 題)

快速排序法(Quick Sort|只需認識)

除錯/填空(5 題)

自行實作(1 題)

計數排序法(Counting Sort|增潤)

除錯/填空(5 題)

自行實作(1 題)

基數排序法(Radix Sort|增潤)

除錯/填空(5 題)

自行實作(1 題)

總結比較:如何選擇檢索與排序?

重點

  • 不同方法的效率主要由時間複雜度(例如 O(n)O(n log n)O(n²))描述。
    Python 小練習:用表格比較成長速度

    以下用簡單估算比較三種增長速度。留意:n 變大時, 會急速變得很大。

  • 除速度外,也要考慮:是否需要額外記憶體、是否穩定排序、以及是否容易實作/除錯。
    Python 小練習:比較「原地排序」與「傳回新排序結果」

    A.sort() 會直接改動原本陣列;sorted(A) 會傳回新陣列。

  • 選擇方法時可用一個簡單準則:資料小/無序 → 線性檢索;資料已排序且很大 → 對分檢索;排序方面,視資料特性選擇合併排序法/快速排序法等。
    Python 小練習:比較線性檢索 vs 對分檢索的「最壞步數」

    下面只做「步數估算」:線性檢索最壞是 n;對分檢索最壞大約是 log₂(n)+1。

如果你只想用一句話記住選擇方法的思路,可以這樣想:資料未排序而且只做少量查找 → 線性檢索;資料已排序 → 對分檢索;需要大量「是否存在」查找 → 雜湊檢索;資料已排序且分佈較均勻 → 插值檢索;資料量大的一般排序 → 合併排序或快速排序;資料幾乎已排序 → 插入排序通常更合適;值域很小的整數 → 計數排序;固定長度整數/字串 → 基數排序。

最重要的是,你要能用追蹤表把流程說清楚:例如在對分檢索中每次 low/high 如何更新; 在合併排序法中左右半邊如何合併;在快速排序法中 pivot 如何把資料分割。 只要你能清楚描述流程,很多題目自然就能解。