怎麼讀 Big O
Big O 描述的是 n 變大時成本怎麼長,不是實際要花多久:平均和最差、均攤、被省略的常數,各自在什麼時候會騙人。
看什麼
成長速度
O(n log n):合併排序、堆積排序。右邊亮起的函式在 n = 16 時,最內層那一步跑了 80 次。縱軸是對數刻度:每往上一條格線就是一千倍。
資料量 n
| Big O | 步數 | 時間 |
|---|---|---|
| O(1) | 1 | 1.0 奈秒 |
| O(log n) | 20 | 20 奈秒 |
| O(n) | 1,000,000 | 1.0 毫秒 |
| O(n log n) | 19,931,569 | 20 毫秒 |
| O(n²) | 1,000,000,000,000 | 16.7 分鐘 |
| O(2ⁿ) | 10^301,030 | 10^301,013 年(宇宙年齡約 10^10) |
亮起來的是這一步執行的程式碼
// O(1): one step, whatever n is.function constantSteps(n: number): number { return 1;} // O(log n): binary search for a value larger than everything.function logSteps(n: number): number { let lo = 0, hi = n - 1, steps = 0; while (lo <= hi) { steps++; const mid = (lo + hi) >> 1; lo = mid + 1; } return steps;} // O(n): look at every element once.function linearSteps(n: number): number { let steps = 0; for (let i = 0; i < n; i++) steps++; return steps;} // O(n log n): one binary search per element.function linearithmicSteps(n: number): number { let steps = 0; for (let i = 0; i < n; i++) steps += logSteps(n); return steps;} // O(n^2): every pair once.function quadraticSteps(n: number): number { let steps = 0; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) steps++; } return steps;} // O(2^n): every subset of n items.function exponentialSteps(n: number, i = 0): number { if (i === n) return 1; return exponentialSteps(n, i + 1) + exponentialSteps(n, i + 1);}模型假設與範圍
- Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。
什麼時候用
- 比較兩種做法、預估資料變大之後撐不撐得住:先看 Big O 排除不可能的做法,再用實測比常數。
- 面試時每個解法都要講出時間與空間複雜度,並說清楚是平均還是最差。
- n 很小時(幾十個)不要只看 Big O:常數小的 O(n²) 常常比 O(n log n) 快,很多標準函式庫的排序在小陣列上就改用插入排序。
和其他主題的關係
- 延伸閱讀
- 排序資料結構 · 陣列型與指標型
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| 讀陣列的第 i 格、雜湊表查找(平均) | O(1) | O(1) |
| 二分搜尋、平衡樹查找 | O(log n) | O(log n) |
| 掃過一遍、找最大值 | O(n) | O(n) |
| 合併排序、堆積排序 | O(n log n) | O(n log n) |
| 兩層迴圈比對每一對 | O(n²) | O(n²) |
| 列舉所有子集合 | O(2ⁿ) | O(2ⁿ) |
空間:O(1),這裡的範例函式只用幾個計數變數;遞迴的 O(2ⁿ) 例子另外佔用 O(n) 的呼叫堆疊
Big O 實測:n 變大時步數怎麼長
數的是:右邊程式碼最內層那一步實際跑了幾次
| Big O | n = 250 | n = 500 | n = 1,000 | n = 2,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| 二分搜尋 | O(log n) | 8 | 9 | 10 | 11 | ×1.4 (×1.4) |
| 掃過一遍 | O(n) | 250 | 500 | 1,000 | 2,000 | ×8.0 (×8.0) |
| 每個元素一次二分搜尋 | O(n log n) | 2,000 | 4,500 | 10,000 | 22,000 | ×11 (×11) |
| 每一對 | O(n²) | 31,125 | 124,750 | 499,500 | 1,999,000 | ×64 (×64) |
這種表格每個主題都有。讀法:n 每變成 2 倍,O(n) 的步數也變 2 倍,O(n²) 變 4 倍,O(log n) 只多一步。換個說法,在 log-log 圖上的斜率:O(n) 是 1.00、O(n²) 是 2.00、O(log n) 是 0.15。
和其他做法比
| n = 10 | n = 1,000 | n = 10^6 | n = 10^9 | |
|---|---|---|---|---|
| O(1) | 1.0 奈秒 | 1.0 奈秒 | 1.0 奈秒 | 1.0 奈秒 |
| O(log n) | 3.3 奈秒 | 10.0 奈秒 | 20 奈秒 | 30 奈秒 |
| O(n) | 10 奈秒 | 1.0 微秒 | 1.0 毫秒 | 1.0 秒 |
| O(n log n) | 33 奈秒 | 10.0 微秒 | 20 毫秒 | 30 秒 |
| O(n²) | 100 奈秒 | 1.0 毫秒 | 16.7 分鐘 | 32 年 |
| O(2ⁿ) | 1.0 微秒 | 10^285 年(宇宙年齡約 10^10) | 10^301,013 年(宇宙年齡約 10^10) | 10^301,029,979 年(宇宙年齡約 10^10) |
每一步算 1 奈秒(現代 CPU 一次簡單運算的數量級)的換算,不是實測。重點不是確切的秒數,而是同一欄裡不同成長速度之間差了多少個數量級:n 到了百萬,O(n²) 已經要幾十分鐘,O(2ⁿ) 則遠超過宇宙的年齡。
真實世界裡的它
- 二分搜尋 O(log n)、雜湊表查找平均 O(1)、排序 O(n log n):每個主題頁的 Big O 表都是這樣標的。
- 資料庫加索引的意義:查詢從全表掃描 O(n) 變成 B-tree 查找 O(log n)。
- 動態陣列的 push 是均攤 O(1)、快速排序平均 O(n log n) 最差 O(n²):同一個操作,不同的 Big O 說法。
取捨與陷阱
- Big O 省略了常數:同樣是 O(n),陣列和分散的串列加總,實測會差好幾倍(見「被省略的常數」)。
- 平均不等於保證:快速排序在已排序的輸入上會退化成 O(n²),攻擊者也可能故意送出最差情況。
- O(n + m) 不能隨便寫成 O(n):兩個輸入的大小可能差很多,例如圖的點數和邊數。
- 別忘了空間複雜度:遞迴解法的呼叫堆疊也算,深到一定程度就會堆疊溢位。