National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
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

Unit 26

排序與搜尋

線性搜尋、二分搜尋與常見排序
Interaction實作 25 minute(s)

Learning Objectives

  • 說明線性搜尋的做法,以及最壞情況要比較 n 次
  • 說明二分搜尋的前提是資料已經排序,並追蹤每一輪如何排除一半
  • 追蹤選擇排序的每一輪,說明它為什麼是 O(n²)
  • 說出插入排序、氣泡排序、合併排序、快速排序的基本想法與複雜度
  • 判斷什麼情況值得先排序再搜尋

每天都在用的兩件事:找東西與排順序

在購物網站按「價格由低到高」,是排序(sorting);在通訊錄輸入名字找人,是搜尋(searching)。 這兩件事是電腦最常做的工作之一:資料庫、搜尋引擎、試算表、手機相簿,背後都有排序與搜尋的演算法。

這一課要回答的問題是:資料有沒有排好,對搜尋的速度影響有多大?排序本身又要花多少步?

我們會用第 24 課的 Big-O 來比較,並用第 25 課的陣列當作資料的存放方式。

線性搜尋:從頭一個一個比

最直接的做法是從第一個開始,一個一個和目標比較,相等就回報位置;全部比完都不相等,就回報找不到(慣例是回傳 -1)。 這叫線性搜尋(linear search)。

它的好處是不挑資料:沒排序也可以用。缺點是最壞情況(目標在最後一個,或根本不在裡面)要比較 n 次,是 O(n)。

線性搜尋:一格一格往右

在 6 個學號裡找 63 和 50。看上方的 ▲ 一格一格往右移:找 63 時比到第 5 個才找到;找 50 時 6 個全部比完,才能確定它不在裡面。

二分搜尋:每次排除一半

翻字典找一個詞,你不會從第一頁開始翻,而是翻到中間,看那一頁的字在目標前面還是後面,再決定往哪一半找。 這就是二分搜尋(binary search)。它有一個前提:資料必須已經由小到大排好。

做法是用 lo、hi 兩個索引圍出「目標如果存在,一定在這個範圍裡」:

  1. 一開始範圍是整個陣列:lo = 0、hi = n - 1
  2. 看範圍正中間的 mid = (lo + hi) // 2
  3. a[mid] 等於目標:找到了
  4. a[mid] 比目標小:目標只可能在右半邊,lo = mid + 1
  5. a[mid] 比目標大:目標只可能在左半邊,hi = mid - 1
  6. 只要 lo <= hi 範圍裡就還有元素,回到第 2 步;lo > hi 表示範圍空了,目標不在裡面

每比一次就排除一半,和第 24 課的猜數字一樣,最多只要比較約 log₂n 次,是 O(log n)。

二分搜尋:看 lo、mid、hi 怎麼移動

灰掉的格子已經不在搜尋範圍裡。找 66 時,第一輪看中間的 34,比 66 小,左半邊整段被排除,第二輪就找到了。找 20 時,範圍一路縮小,最後 lo 跑到 hi 的右邊(範圍空了),回傳 -1。10 個元素最多只要比 4 次。

兩種搜尋差多少

最壞情況下的比較次數:

資料筆數 n 線性搜尋 二分搜尋
10 10 4
1,000 1,000 10
1,000,000 1,000,000 20
1,000,000,000 1,000,000,000 30

資料越多,差距越大。這也是為什麼字典、電話簿、資料庫的索引都要「排好順序」:排一次,之後的每一次搜尋都能用二分搜尋。

想一想:搜尋

1.

二分搜尋能正確運作,資料一定要符合哪個條件?

2.

一份已經排序的名單有 1,000,000 筆資料,用二分搜尋找一個人,最多要比較幾次?

3.

下面的程式會印出什麼?

a = [7, 3, 9, 3, 5]
count = 0
for x in a:
    count += 1
    if x == 3:
        break
print(count)
第 1 次作答

選擇排序:每一輪挑出最小的

排序最容易理解的方法之一是選擇排序(selection sort),就像整理一手撲克牌時,每次從還沒整理的牌裡挑出最小的,放到最左邊:

  1. 第 1 輪:在全部 n 個數裡找出最小的,和第 0 格交換
  2. 第 2 輪:在剩下的 n - 1 個數裡找出最小的,和第 1 格交換
  3. 依此類推,做 n - 1 輪,全部就排好了

每一輪都要把剩下的數全部看過,比較次數是 (n - 1) + (n - 2) + … + 1 = n × (n - 1) ÷ 2,和第 24 課的握手次數一樣,是 O(n²)。

選擇排序:一輪一輪看

長條的高度就是數值。▲ i 是這一輪要放的位置,▲ j 往右掃描,「目前最小」記住掃描到現在最小的那一格。每一輪結束時交換一次,左邊排好的部分就多一格。

其他常見的排序方法

排序的方法很多,下面是最常聽到的幾種:

方法 想法 複雜度
選擇排序 每輪挑出剩下最小的,放到前面 O(n²),不管資料長怎樣都要比完
插入排序 像整理手上的牌:拿起下一張,往左插到正確位置 最壞 O(n²);資料幾乎排好時很快,接近 O(n)
氣泡排序 相鄰兩個比較,順序錯就交換,大的會像氣泡一樣浮到最後 最壞 O(n²)
合併排序 把資料切成兩半,各自排好,再把兩個排好的部分合併 O(n log n)
快速排序 挑一個基準值,比它小的放左邊、大的放右邊,兩邊再各自排 平均 O(n log n);基準選得很差時是 O(n²)

合併排序和快速排序都用了分而治之(divide and conquer)的想法:把大問題切成小問題,小問題解完再組合起來。 合併排序一般認為最早由馮紐曼(von Neumann)在 1945 年寫出,他也是第 10 課「馮紐曼架構」的那位馮紐曼。

O(n²) 和 O(n log n) 在資料多時差很多:排序一百萬筆資料,n² 是 10¹² 步,n log₂n 大約只有 2 × 10⁷ 步。 實際寫程式時,直接用語言內建的排序就好(例如 Python 的 sorted() 和 list.sort()),它們都是 O(n log n) 而且經過高度最佳化。

下面的小工具讓幾種排序在同一份資料上賽跑,每一步是一次比較或一次交換(移動、寫入)。

互動練習:排序賽跑

選資料型態(隨機、已排序、反向…)、用「資料大小」滑桿調整筆數,按「▶ 開始」或「單步」。四種排序同時各走一步,操作次數越少的越早跑完;每個面板下方會顯示比較次數與交換(移動、寫入)次數。照著任務做:先用隨機資料跑一次;再換成已排序、反向的資料,回答出現的問題;最後把資料大小調到 30 以上,用隨機資料看哪一種的比較次數最少。

從賽跑看到的事

  • 資料已經排好時,插入排序和氣泡排序很快就發現「不用動」,選擇排序卻還是要把 n × (n - 1) ÷ 2 次比較全部做完
  • 選擇排序每一輪最多只交換一次,所以交換次數最少;如果「搬動資料」很花時間,這是它的優點
  • 資料只有 10 筆時,合併排序的比較次數不一定最少;調到 30 筆以上,O(n log n) 的合併排序就明顯比三種 O(n²) 的排序少,筆數越多差距越大
  • 沒有一種排序在每種情況都最好,這也是為什麼要分析演算法:看資料的特性與最在意的成本來選

想一想:排序

1.

用選擇排序排 5 個數,下面的程式會印出什麼?

a = [5, 1, 4, 2, 3]
n = len(a)
comparisons = 0
for i in range(n - 1):
    smallest = i
    for j in range(i + 1, n):
        comparisons += 1
        if a[j] < a[smallest]:
            smallest = j
    a[i], a[smallest] = a[smallest], a[i]
print(comparisons)
2.

排序 1,000,000 筆資料,O(n²) 的方法大約要 10¹² 步。O(n log n) 的方法大約要幾步?

3.

一份沒有排序的名單,你只需要找一次某個人。先排序再用二分搜尋,還是直接線性搜尋比較划算?

第 1 次作答

實作:完成二分搜尋

圖書館的書依編號排好放在 books 裡。binary_search 已經會算 mid、也會在找到時回傳,請完成 TODO:a[mid] 比目標小時要往右半邊找,比目標大時要往左半邊找。完成後記得刪掉 break 那一行,再按「執行並檢查」。

你的輸出
預期輸出

重點整理

  • 線性搜尋從頭一個一個比,不需要排序,最壞 n 次,O(n)
  • 二分搜尋要求資料已經排序,每次看中間、排除一半,最多約 log₂n 次,O(log n)
  • 選擇排序每輪挑出剩下最小的,比較次數 n × (n - 1) ÷ 2,O(n²);插入排序、氣泡排序最壞也是 O(n²)
  • 合併排序、快速排序用分而治之,O(n log n)(快速排序是平均情況);實際寫程式用語言內建的排序
  • 只找一次就直接線性搜尋;要找很多次,先排序一次再用二分搜尋比較划算

下一課:資料庫。資料庫為了讓查詢變快,也會先替資料建立排好順序的「索引」,用的正是這一課的想法。 想看更多排序方法的細節與程式,可以接著上演算法軌(第 04、06~09 課)。

這一課完成了!
Next lesson:資料庫概念與資料模型
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。