🎯 什麼情境該想到我

當你想判斷「這段程式在資料量變大時會不會爆掉」,或在兩個方案間比較效率時。

⚙️ 怎麼用

  1. 看主導項:只看輸入 n 變大時成長最快的部分,忽略常數與低階項。
  2. 常見等級(由快到慢):O(1) → O(log n) → O(n) → O(n log n) → O(n²) → O(2ⁿ)。
  3. 快速估:單層迴圈 O(n)、巢狀雙迴圈 O(n²)、每次砍半 O(log n)、排序多為 O(n log n)。
  4. 同時看空間複雜度:記憶體也可能是瓶頸(時間換空間的取捨)。

先估複雜度,再決定「資料規模下這個方案可不可行」,避免上線才發現慢爆。

★ 從程式碼讀出複雜度:四個模式

程式碼的形狀複雜度注意
一層迴圈跑 n 次O(n)
兩層巢狀,內層也跑 n 次O(n²)
兩層巢狀,內層從 i 開始(for j := i; j < n; j++)仍是 O(n²)總次數 ,常數減半但階數不變
每次把問題砍半(n /= 2)O(log n)二分搜尋、平衡樹
外層 n 次,內層砍半O(n log n)
兩個獨立的輸入O(n + m) 或 O(n·m)不要合併成 n:for a in A { for b in B } 是 O(n·m)

★ Go 裡藏起來的成本:看起來是 O(1),其實不是

寫法實際成本為什麼
迴圈裡 s += x(字串)O(n²) 總計字串不可變,每次 += 都配置新字串並複製全部內容 → 用 strings.Builder
迴圈裡 slices.Contains(list, x)O(n·m)Contains 是線性搜尋 → 先把 list 轉成 map[T]struct{}
迴圈裡 append攤還 O(1)但偶爾一次 O(n) 的擴容;知道大小就 make([]T, 0, n)(見 工具-攤還分析)
copy、slices.Clone、傳大陣列(非 slice)當參數O(n)陣列是值型別,傳參數會整個複製;slice 只複製 header
len(s)、len(m)O(1)長度是存起來的
把 string 轉 []byte(或反過來)通常 O(n)需要複製,除非編譯器能證明不需要
在 slice 開頭插入或刪除O(n)要搬移後面所有元素;需要頻繁操作頭部就用環形緩衝區

TS 對應:array.includes()、indexOf()、shift()、unshift()、splice() 都是 O(n);在迴圈裡用它們一樣會變成 O(n²)。

★ 用限制條件反推可行的複雜度

一秒大約能做 量級的簡單操作(經驗法則,實際取決於操作種類與語言)。反推:

n 的上限可以接受的複雜度常見的解法
≤ 10O(n!)列舉所有排列
≤ 20–25O(2ⁿ)列舉子集、位元遮罩 DP
≤ 500O(n³)三層迴圈、Floyd-Warshall
≤ 5,000O(n²)兩層迴圈、二維 DP
≤ 10⁶O(n log n)排序、堆積、二分搜尋
≤ 10⁸O(n)一次走訪、雙指標
更大O(log n) 或 O(1)數學公式、二分搜尋

這張表在面試時特別有用:看到 n ≤ 10⁵,就知道 O(n²) 不行,不用寫完才發現超時。

★ Big-O、Θ、Ω 的差別(和為什麼平常說的 O 其實是 Θ)

  • O(g):上界——「最多這麼慢」。O(n) 的演算法技術上也是 O(n²),因為 n² 也是上界。
  • Ω(g):下界——「至少這麼慢」。例如比較式排序是 Ω(n log n),不可能更快。
  • Θ(g):同時是上界和下界——「剛好是這個量級」。

日常說「合併排序是 O(n log n)」時,意思其實是 Θ(n log n)。這個區別在證明下界時才重要:說「這個問題的任何解法都是 Ω(n log n)」,才能證明你不可能再做得更好。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意

  • Big-O 是趨勢不是絕對;小 n 時常數/實作細節可能主導,別過早為漸進最佳而犧牲可讀性(見 工具-簡單清楚至上)。

🔗 相關工具