同一個問題有很多寫法。「哪個好?」不能只用碼表量:機器快慢、資料大小都會干擾。 演算法分析問的是:當輸入規模 n 變大,工作量怎麼成長? 我們數「基本操作次數」T(n),只留下成長最快的主導項。
T(n) = 5n² + 27n + 1005 → 當 n 夠大,n² 說了算:O(n²)。 常數與低次項在成長率面前都是雜訊。
| f(n) | 名稱 | n=10 | n=1,000 | n=10⁶ | 典型例子 |
|---|---|---|---|---|---|
| 1 | 常數 | 1 | 1 | 1 | 索引 a[i]、hash 查詢(平均) |
| log n | 對數 | 3 | 10 | 20 | binary search |
| n | 線性 | 10 | 1,000 | 10⁶ | 走訪、sequential search |
| n log n | 線性對數 | 33 | 10⁴ | 2×10⁷ | merge sort |
| n² | 平方 | 100 | 10⁶ | 10¹² | 雙層迴圈、bubble sort |
| 2ⁿ | 指數 | 1,024 | 10³⁰¹ | — | 天真費波那契、河內塔 |
拿一段真的程式來數。三個賦值(3)、外圈跑 n 次的內圈各貢獻 n 次(3n²… 這裡照講義的例子): T(n) = 3 + 3n² + 2n + 1 = 3n² + 2n + 4。 n 一大,3n² 說了算,其他項連同係數 3 全部丟掉:O(n²)。 數的時候不用精確到每一行,抓住「哪一段被執行最多次」就夠了。
有些演算法的表現不只看 n,還看資料內容。在陣列裡找一個值: 運氣好第一格就中(best case,O(1))、運氣差找到最後一格或根本不在(worst case,O(n))、 平均攤下來要看一半(average case)。講 Big-O 時預設講 worst case,因為它是保證; 第 6 章的雜湊表會是「平均 O(1)、最差 O(n)」這種分開報的典型例子。
for (i=0;i<n;i++) for (j=0;j<n;j++) k++; 之後接 for (i=0;i<n;i++) k++;,整段的 Big-O 是?
「earth 和 heart 是不是變位詞?」四種解法都對,但成長率天差地遠。 這是「正確 ≠ 好」的最佳教材。
解法 2「排序後比較」看起來只有一個迴圈,為什麼不是 O(n)?
vector 底層是連續陣列。索引 O(1);
push_back 平常 O(1),偶爾裝滿要翻倍搬家:把搬家成本平均到每次操作,仍是 O(1)(攤銷)。
但 insert(begin(), x) 每次都要整體右移 → O(n)。
pop_back() 平坦不動、erase(begin()) 隨 n 線性爬升。unordered_map(雜湊表)讓「查 key」平均 O(1):不用排序、不用走訪。
細節(hash 函數、碰撞、載入因子)第 6 章展開;本章先記住成本表與「平均」兩個字的份量。
| 操作 | vector(無序) | vector(已排序) | unordered_map/set |
|---|---|---|---|
| 查找 contains(x) | O(n) | O(log n) | O(1) 平均/O(n) 最差 |
| 插入 | O(1) 尾端 | O(n)(挪位) | O(1) 平均 |
| 刪除 | O(n) | O(n) | O(1) 平均 |
| 依序輸出 | 需先排序 | O(n) | —(無序!) |
map。unordered_map。T(n) = 3 + 100·log n + 0.001·n² 的 Big-O 是?
for (i=0;i<n;i++) for (j=i;j<n;j++) k++; 執行 k++ 幾次?Big-O?
int i = 1; while (i <= n) {{ for (j = 1; j <= i; j++) x++; i *= 2; }} 的複雜度是?
亂序數列找第 k 小的數,要求 O(n log n)。最直接的做法是?
需求:不斷插入單字、隨時問「這個單字出現過嗎」。10⁶ 次操作,選哪個?
| 本章對應 | cppds | 自學頁 |
|---|---|---|
| 什麼是演算法分析 | §2.1–2.2 | P00 |
| Big-O | §2.3 | P01–P02 |
| Anagram | §2.4 | P03 |
| C++ 集合效能(vector/string) | §2.5–2.7 | P04 |
| Hash Tables | §2.8 | P05 |