跳到主要內容

演算法

同一個問題,不同的解題思路

主題 · 怎麼讀 Big O

怎麼讀 Big O

Big O 描述的是 n 變大時成本怎麼長,不是實際要花多久:平均和最差、均攤、被省略的常數,各自在什麼時候會騙人。

看什麼
成長速度
110^310^610^918162432nO(1)O(log n)O(n)O(n log n)O(n²)O(2ⁿ)

O(n log n):合併排序、堆積排序。右邊亮起的函式在 n = 16 時,最內層那一步跑了 80 次。縱軸是對數刻度:每往上一條格線就是一千倍。

資料量 n
n = 1,000,000,每一步 1 奈秒
Big O步數時間
O(1)11.0 奈秒
O(log n)2020 奈秒
O(n)1,000,0001.0 毫秒
O(n log n)19,931,56920 毫秒
O(n²)1,000,000,000,00016.7 分鐘
O(2ⁿ)10^301,03010^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 On = 250n = 500n = 1,000n = 2,000成長倍數:實測(理論)
二分搜尋O(log n)891011×1.4 (×1.4)
掃過一遍O(n)2505001,0002,000×8.0 (×8.0)
每個元素一次二分搜尋O(n log n)2,0004,50010,00022,000×11 (×11)
每一對O(n²)31,125124,750499,5001,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 = 10n = 1,000n = 10^6n = 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):兩個輸入的大小可能差很多,例如圖的點數和邊數。
  • 別忘了空間複雜度:遞迴解法的呼叫堆疊也算,深到一定程度就會堆疊溢位。