① 照節次讀:每一節先讀說明、動手玩互動元件,預測結果再按按鈕驗證。 ② 對照講義:頁上 §徽章對應 cppds 章節;細節與完整程式請回講義 02 與 cppds 原文。 ③ 每節做 quiz:答錯就回到該節重讀,不要往下跳。 ④ 最後翻 關鍵詞彙卡(14 張)自測術語(中文為主、術語附英文),並用 REF 總覽區當速查表。標「講義補充」的節是課堂沒細講的延伸,第一輪可略過。
同一個問題有很多寫法。「哪個好?」不能只用碼表量:機器快慢、資料大小都會干擾。 演算法分析問的是:當輸入規模 n 變大,工作量怎麼成長? 我們數「基本操作次數」T(n),只留下成長最快的主導項。
| n | 迴圈法(次加法) | 高斯公式(次運算) | 比值 |
|---|---|---|---|
| 1,000 | 1,000 | 3 | 333× |
| 100,000 | 100,000 | 3 | 33,333× |
| 10,000,000 | 10,000,000 | 3 | 3,333,333× |
55
55
foo 和 sumOfN 做的事一模一樣、效率也一樣:可讀性跟效率是兩回事。演算法分析比的是後者:同一個問題,不同「解法」消耗的資源。
55
steady_clock 是單調時鐘,適合量耗時(system_clock 會被校時影響)。量出來的秒數用傳參考的 seconds 帶回:一個函式想「回傳兩個值」時的慣用手法。
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²)。 數的時候不用精確到每一行,抓住「哪一段被執行最多次」就夠了。
常用計數公式:$\sum_{i=1}^n 1=n$,而 $\sum_{i=1}^n i=n(n+1)/2\in\Theta(n^2)$。 前者對應單層固定工作,後者常出現在逐輪縮短的巢狀迴圈。
有些演算法的表現不只看 n,還看資料內容。在陣列裡找一個值: 運氣好第一格就中(best case,O(1))、運氣差找到最後一格或根本不在(worst case,O(n))、 若假設目標位置均勻分布,期望看一半(average case)。Big-O 是「上界」記號,不等於 worst case; 應先說明分析哪一種輸入情況,再為那個成本函數寫界。average case 也不等於 amortized: 後者不假設隨機輸入,而是把一串操作中偶發的昂貴成本攤回每次操作。
for (i=0;i<n;i++) for (j=0;j<n;j++) k++; 之後接 for (i=0;i<n;i++) k++;,整段的 Big-O 是?
逐項數:開頭 3 個指定;雙層迴圈本體 3 個指定,各跑 n² 次;單層迴圈 2 個指定,跑 n 次;收尾 1 個。T(n) = 3 + 3n² + 2n + 1,只留主導項就是 O(n²)。這題是講義的自我檢測題,先自己數完再看這段解說。
「earth 和 heart 是不是變位詞?」四種解法都對,但成長率天差地遠。 這是「正確 ≠ 好」的最佳教材。
解法 2「排序後比較」看起來只有一個迴圈,為什麼不是 O(n)?
vector 底層是連續陣列。索引 O(1);
push_back 平常 O(1),偶爾裝滿要搬到較大的緩衝區。課堂動畫用容量加倍說明幾何成長。
但 insert(begin(), x) 每次都要整體右移 → O(n)。
pop_back() 平坦不動、erase(begin()) 隨 n 線性爬升。insert at front 285.31 ms push_back 6.42 ms with reserve 3.85 ms direct index 2.10 ms
前端插入每次都要搬動整段資料,所以慢兩個量級。push_back 偶爾要搬家;容量加倍是課堂模型,C++ 標準不規定倍率,只保證攤還 O(1)。先 reserve 可避免這次建表過程中的重新配置。
n erase(begin) pop_back 2500000 155.20031 0.00022 5000000 311.87542 0.00021 7500000 468.11289 0.00023 10000000 625.40067 0.00022
從需要做的工作看成本:erase(begin()) 要把後續元素往前搬,單次操作是 O(n);pop_back() 只移除尾端元素,單次操作是 O(1)。下圖比較兩種操作隨資料量增加的耗時。
C++11 起,std::string 的字元連續儲存。索引與 size() 是 O(1),
尾端加入是攤還 O(1);中間插入或刪除最差 O(n),因為後續字元需要搬移。
| 操作 | 複雜度 | 原因 |
|---|---|---|
| s[i] / s.size() | O(1) | 連續儲存與直接位址計算 |
| s.push_back(ch) | 攤還 O(1) | 偶爾重新配置並搬移整串 |
| s.insert / s.erase | 最差 O(n) | 搬移操作位置後方的字元 |
C++ data! | size=9
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) | —(無序!) |
| 容器 | 估計比較/探查次數(10 萬元素查 1000 次) |
|---|---|
| vector 線性查找 | 約 50,000,000 次 |
| 排序 vector + binary search | 約 17,000 次 |
| unordered_set | 約 1,200 次(平均每查 1.2 次探查) |
map。unordered_map。n vector hash table 250000 8.512 0.011 500000 17.204 0.012 1000000 35.917 0.012
vector 的 find 逐一比對元素,搜尋成本隨資料量增加。雜湊表的 count 利用雜湊值定位;雜湊分布良好、負載因子受控時,平均查詢成本為 O(1),最差為 O(n)。
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⁶ 次操作,選哪個?
只保留本章已出現的結構;$n$ 是目前元素或字元數。「—」表示固定大小容器沒有該操作。
| Ch2 結構 | 存取 | 搜尋 | 插入 | 刪除 | 總儲存量 |
|---|---|---|---|---|---|
std::array<T,N> | 索引 O(1) | 線性掃描 O(n) | — 固定大小 | — 固定大小 | O(n) |
std::vector<T> | 索引 O(1) | 依值 O(n) | 尾端攤銷 O(1);前/中 O(n) | 尾端 O(1);前/中 O(n) | O(n),capacity 可大於 size |
std::string | 索引 O(1) | 單字元掃描 O(n);子字串依演算法 | 單字元尾加攤銷 O(1);前/中 O(n) | 尾端 O(1);前/中 O(n) | O(n) |
std::unordered_map<K,V> | 用 key,非索引 | key:平均 O(1)、最壞 O(n) | 平均 O(1)、最壞 O(n) | 平均 O(1)、最壞 O(n) | O(n) entries + buckets |
vector/string 尾端加入的 O(1) 是攤銷;
unordered_map 的 O(1) 是平均,最壞仍可退化成 O(n)。兩者不是同一種保證。
string key 的 hash/比較時間還要另外計入 key 長度。
學生延伸參考: C++ Data Structures and Algorithms Cheat Sheet 還涵蓋後續章節的結構。原頁是精簡速查;考試與作業以本課程校正版及各章定義為準。
| 本章對應 | cppds | 自學頁 |
|---|---|---|
| 什麼是演算法分析 | §2.1–2.2 | P00 |
| Big-O | §2.3 | P01–P02 |
| Anagram | §2.4 | P03 |
| C++ vector | §2.5–2.6 | P04 |
| C++ string | §2.7 | P05 |
| Hash Tables | §2.8 | P06 |
詞彙卡取自本章課程題庫,已譯為繁體中文,正面附英文原名。先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。