用 O(n³) 解 n=10⁶ 的問題
最直接的反例:三層迴圈解排列問題,n=100 時需要 10⁶ 次操作,n=1000 時需要 10⁹ 次,n=10⁶ 時需要 10¹⁸ 次——宇宙熱寂都等不完。
根本問題:設計時沒有估算輸入規模。在實作之前,先問「n 會是多少?」:
- 用戶數:可能是 10^6-10
- 每個用戶的資料點:可能是 10^2-10
- 這個函式每秒被呼叫幾次?
不同的 n 範圍對應不同可接受的複雜度:
| n | 可接受的最大複雜度 |
|---|---|
| ≤ 10 | O(n!) 都行 |
| ≤ 100 | O(n³) |
| ≤ 10^4 | O(n²) |
| ≤ 10^6 | O(n log n) |
| ≤ 10^8 | O(n) |
對可排序資料用 Hash,應該用 Tree
Hash Map 的查詢是 O(1),比 BST 的 O(log n) 快,所以「總是用 Hash」的直覺是錯的。
需要 Tree 的場景:
- 範圍查詢(「找所有 age 在 18-25 之間的 user」)——Hash 不支援範圍查詢
- 排序遍歷(「按 score 從高到低輸出」)——Hash 沒有順序
- Floor / Ceiling 操作(「找大於等於 key 的最小 element」)
強行用 Hash 做這些需求:每次查詢要 O(n) 遍歷所有 element,比用 BST 的 O(log n) 還差。
每次 Request 重新計算相同的結果
API endpoint 被每秒呼叫 1000 次,每次都從頭計算「全站過去 30 天的銷售統計」——這個查詢需要掃描 1 億條記錄。結果是 DB 100% CPU,所有 API 都 timeout。
根本解法:識別哪些計算結果是穩定的(或可以接受輕微過時),用 Cache 存起來(Redis、Memcached、應用層 in-memory cache)。
cache 的維度:TTL(結果多久過期)、緩存粒度(緩存整個查詢結果 vs 緩存中間計算)、緩存失效策略(寫入時失效 vs 定時失效)。
DP 狀態沒有壓縮,記憶體爆了
背包問題的 DP 陣列是 dp[n][W],n=10^4, W=10^6——需要 10^10 個 element,幾十 GB 的記憶體。
解法:滾動陣列(Rolling Array)
很多 DP 問題的狀態轉移只依賴「前一行」,不需要保留整個 dp 陣列:
# 2D DP:O(n*W) 空間
dp = [[0] * (W+1) for _ in range(n+1)]
# 空間優化:只保留前一行
dp = [0] * (W+1)
for item in items:
for w in range(W, item.weight-1, -1): # 倒序避免重複選
dp[w] = max(dp[w], dp[w-item.weight] + item.value)空間從 O(n*W) 降到 O(W)。
Bloom Filter 當 Exact Lookup 用
Bloom Filter 的特性:沒有 false negative,但有 false positive——如果 filter 說「不存在」,那一定不存在;但如果說「存在」,可能是誤報。
錯誤用法:用 Bloom Filter 作為資料存在的確定性判斷——把它當 Hash Set 用。有 false positive 的話系統行為不正確。
正確用法:作為「昂貴操作的前置過濾器」——先問 Bloom Filter「有沒有可能存在」,不可能存在就直接跳過(省掉 DB 查詢);可能存在才去 DB 確認。
請求 → Bloom Filter 說「不存在」→ 直接返回 404(準確)
請求 → Bloom Filter 說「可能存在」→ 查 DB 確認(可能是 false positive,但不影響正確性)
B+ Tree 硬用在 In-Memory 場景
B+ Tree 的設計優化是磁碟 I/O 最小化:大 node、locality 好、sequential read 友善。這些特性在記憶體裡沒有意義(記憶體沒有 page 的概念)。
In-memory 場景用 Hash Table(O(1) 查詢)或 Red-Black Tree / Skip List(有序操作需求)。把 B+ Tree 硬塞進 in-memory 的結果是:pointer-heavy 的資料結構、cache unfriendly、不必要的複雜度。
Recursion 沒有 Memoization,指數爆炸
Fibonacci 的純遞迴是教科書反例:
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2) # O(2^n)fib(50) 需要 10^15 次呼叫。
任何有重疊子問題的遞迴(相同的參數被重複計算),加上 Memoization 就從指數降到多項式:
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2) # O(n)識別「這個遞迴有沒有重疊子問題」是判斷需不需要 DP / Memoization 的關鍵問題。