National Chengchi 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 a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

政治大學 AQI 58 25°C PM2.5 12
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 02

時間複雜度與 Big-O

數基本運算的次數、成長曲線,以及最好、最壞、平均情況
Interaction實作 25 minute(s)

Learning Objectives

  • 數出單層迴圈、雙層迴圈、三角形雙層迴圈與「每次減半」迴圈的基本運算次數
  • 用 Big-O 描述步驟數的成長:去掉常數與低次項,只留最高次項
  • 比較 O(1)、O(log n)、O(n)、O(n log n)、O(n²)、O(2ⁿ) 在 n 變大時的差距
  • 分辨同一個演算法的最好、最壞、平均情況
  • 說明空間複雜度:演算法額外用了多少記憶體

資料變大時,程式會慢多少

你寫了一支程式檢查班上 40 個人裡有沒有兩個人學號重複,一下就跑完了。換成全校兩萬人,它會慢 500 倍?還是慢 25 萬倍?還是根本跑不完?

上一課說過,分析演算法不看秒數,而是數基本運算的次數。這一課要做兩件事:

  1. 看程式的結構(幾層迴圈、迴圈怎麼變化),數出步驟數和 n 的關係
  2. 用 Big-O 把這個關係寫成簡短的式子,例如 O(n)、O(n²),一眼看出 n 變大時會慢多少

這裡的 n 是輸入的大小:陣列的長度、要排序的資料筆數、圖的節點數等等。

單層迴圈與雙層迴圈

single_loop(n) 的迴圈跑 n 輪,count 數到 n。double_loop(n) 外層每跑一輪,內層就完整跑 n 輪,所以是 n × n 次。n 從 4 變成 8(2 倍),單層從 4 次變成 8 次(2 倍),雙層從 16 次變成 64 次(4 倍)。

每一對只比一次:三角形的雙層迴圈

開頭的問題「有沒有兩個人學號重複」,最直接的做法是每兩個人比一次。 內層迴圈的 j 從 i + 1 開始,因為「第 2 個和第 5 個」比過了,就不必再比「第 5 個和第 2 個」,自己和自己也不用比。

這樣內層的輪數一輪比一輪少:i = 0 時比 n - 1 次,i = 1 時比 n - 2 次……最後一輪 0 次。加起來是

(n - 1) + (n - 2) + … + 1 + 0 = n(n - 1) / 2

把 i、j 的組合畫成表格,比過的格子剛好是對角線右上方的三角形,所以常叫三角形的雙層迴圈。

每兩個比一次,數比較次數

看 j 永遠在 i 的右邊。i = 0 時 j 走 5 格,i = 1 時走 4 格……6 個元素總共比 5 + 4 + 3 + 2 + 1 = 15 次,和公式 n(n - 1) / 2 相同。索引 1 和 3 的 7 是唯一相同的一對。

每次砍一半:log n

有些迴圈不是每輪加 1,而是每輪把問題砍掉一半,例如上一課「每次猜中間」的猜數字。這種迴圈跑的輪數,是「n 要除以 2 幾次才會剩 1」,也就是 log₂n。

n 對半幾次剩 1
8 3
1,000 9
1,000,000 19

n 變大一百萬倍,次數只多十幾次。log₂n 是演算法裡最常出現的「慢慢長」的函數。

數一數要對半幾次

看上方的 n 每一輪變成一半(整數除法,捨去小數)。1,000 只要 9 次就剩 1,1,000,000 也只要 19 次。

Big-O:只看最高次項

真實的程式不會只有一個迴圈。假設數出來某個演算法要做

3n² + 5n + 2 次基本運算

n 很大時,3n² 遠遠超過 5n + 2:n = 1,000 時,3n² 是 300 萬,5n + 2 只有 5,002。而前面的係數 3 是由「一次比較算一步還是兩步」這類細節決定的,換個語言或寫法就會變。 所以描述成長率時,我們去掉常數係數、去掉低次項,只留最高次項,寫成 O(n²),讀作「Big-O of n 平方」或「n 平方等級」。

幾個例子:

數出來的步驟數 Big-O
2n + 10 O(n)
n(n - 1) / 2 = 0.5n² - 0.5n O(n²)
4 log₂n + 7 O(log n)
3n log₂n + 20n O(n log n)
12(和 n 無關) O(1)

嚴格的定義是:如果存在常數 c 和 n₀,使得 n ≥ n₀ 時步驟數都 ≤ c × g(n),就說步驟數是 O(g(n))。白話說,O(g(n)) 是一個上界:n 夠大以後,步驟數不會比 g(n) 的某個倍數還多。 log 的底數也可以省略,因為 log₂n 和 log₁₀n 只差一個常數倍。

O 這個符號由數學家 Paul Bachmann 在 1894 年提出、Edmund Landau 推廣,原本用在數論;1976 年 Donald Knuth 在〈Big Omicron and Big Omega and Big Theta〉一文中提倡把它用在演算法分析,之後成為電腦科學的標準寫法。

常見的成長率,由慢到快:

Big-O 名稱 典型例子
O(1) 常數時間 用索引讀取陣列元素、1 加到 n 的公式
O(log n) 對數時間 二分搜尋(第 04 課)
O(n) 線性時間 找最大值、線性搜尋
O(n log n) 線性對數時間 合併排序(第 07 課)
O(n²) 平方時間 每兩個比一次、簡單排序(第 06 課)
O(2ⁿ) 指數時間 列出所有子集合

下面的小工具把這幾條曲線畫在一起,用滑桿把 n 拉大,看它們怎麼分開。

成長曲線比一比

勾選要比較的函數,拉動 n。表格列出每個函數在目前的 n 是多少次運算,以及電腦每秒做 10⁸(一億)次運算時大約要多久。照著任務做:先找 n² 和 n log₂n 的差距,再看 2ⁿ 有多可怕,最後切換對數刻度。

從小工具看到的事

  • n 很小的時候(例如 n = 5),n² 是 25、n log₂n 約 11.6,只差兩倍多,看不出什麼。Big-O 談的是 n 很大時的趨勢
  • n² 和 n log₂n 的比值是 n ÷ log₂n,n 越大差越多;要到 n = 997,n² 才第一次超過 n log₂n 的 100 倍。資料有幾百萬筆時,O(n²) 和 O(n log n) 的排序可能就是「幾秒」和「幾天」的差別
  • 2ⁿ 在 n = 50 時,每秒一億次也要算一百多天;n 只要再加 10,時間就變成 2¹⁰ = 1,024 倍,要好幾百年。指數時間的演算法只能處理很小的 n
  • 線性刻度下,最大的那條曲線會把其他曲線壓得貼在地上;對數刻度把「乘以 10」畫成同樣的高度,才看得出每條曲線的相對位置

想一想:這段程式是哪一級

1.

下面的程式,時間複雜度是多少?

for i in range(n):
    for j in range(10):
        total += a[i] * j
2.

某個演算法要做 5n² + 300n + 1000 次運算,用 Big-O 表示是?

3.

這段程式會印出什麼?

n = 5
count = 0
for i in range(n):
    for j in range(i):
        count += 1
print(count)
4.

下面的迴圈,時間複雜度是多少?

i = 1
while i < n:
    i = i * 2
第 1 次作答

最好、最壞、平均情況

同一個演算法、同樣大小的 n,步驟數還可能隨資料的內容而不同。例如「找第一個不及格的成績」:

  • 最好情況(best case):第一個就不及格,比 1 次
  • 最壞情況(worst case):沒有人不及格,n 個全部比完
  • 平均情況(average case):在所有可能的輸入上取平均,需要假設資料怎麼分布

分析時通常看最壞情況,因為它是保證:再怎麼倒楣都不會超過這個次數。所以我們說這個演算法是 O(n),雖然運氣好時只要 1 次。

同一個演算法,三種情況

三組成績都是 6 個。第一組第 0 個就是 45,比 1 次;第二組在索引 3 找到 52,比 4 次;第三組全部及格,6 個都要比。

空間複雜度:額外用了多少記憶體

除了時間,演算法也會用記憶體。空間複雜度(space complexity)描述的是演算法額外需要多少記憶體(輸入本身不算),一樣用 Big-O 表示。

def total(a):            # 額外空間 O(1):不管 a 多長,只多用了 s 和 x 兩個變數
    s = 0
    for x in a:
        s += x
    return s


def doubled(a):          # 額外空間 O(n):建立一個和 a 一樣長的新串列
    result = []
    for x in a:
        result.append(x * 2)
    return result

時間和空間常常可以互相交換:多用一些記憶體記住算過的結果,就能少算很多次(第 12 課的雜湊表、第 21 課的動態規劃都是這個想法)。 遞迴呼叫也會用到記憶體,每一層呼叫都要記住自己的變數,第 05 課會看到。

想一想:情況與空間

1.

一個 O(n²) 的程式處理 1,000 筆資料要 2 秒。資料變成 3,000 筆,大約要多久?

2.

關於「線性搜尋在最好情況只要比 1 次」,下面哪個說法正確?

3.

把一個長度 n 的陣列「原地」反轉(只用兩個索引從兩端往中間交換),額外空間是多少?

第 1 次作答

實作:n 變 2 倍,次數變幾倍

請完成 count_pairs(n):用三角形的雙層迴圈(j 從 i + 1 開始)數出「每兩個比一次」總共要比幾次。程式會印出 n = 4、8、16 的結果,並和公式 n(n - 1) / 2 對照。觀察 n 每變成 2 倍,次數大約變成幾倍。

你的輸出
預期輸出

重點整理

  • 數基本運算的次數:單層迴圈 n 次、雙層迴圈 n² 次、每一對只比一次的三角形迴圈 n(n - 1) / 2 次、每次砍一半的迴圈 log₂n 次
  • Big-O 去掉常數係數與低次項,只留最高次項:3n² + 5n + 2 是 O(n²);它描述的是 n 很大時步驟數的上界
  • 常見的成長率由慢到快:O(1)、O(log n)、O(n)、O(n log n)、O(n²)、O(2ⁿ);n 很大時差距非常懸殊,2ⁿ 只能處理很小的 n
  • 同一個演算法有最好、最壞、平均情況;通常看最壞情況,因為它是保證
  • 空間複雜度看演算法額外用了多少記憶體,例如原地處理是 O(1)、建立同樣長的新陣列是 O(n)

下一課:陣列與串列的操作成本。用索引讀取為什麼是 O(1)?在最前面插入為什麼是 O(n)?Python 的串列一直 append,為什麼平均還是很快?

這一課完成了!
Next lesson:陣列與串列的操作成本
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。