用 O(n³) 解 n=10⁶ 的問題

最直接的反例:三層迴圈解排列問題,n=100 時需要 10⁶ 次操作,n=1000 時需要 10⁹ 次,n=10⁶ 時需要 10¹⁸ 次——宇宙熱寂都等不完。

根本問題:設計時沒有估算輸入規模。在實作之前,先問「n 會是多少?」:

  • 用戶數:可能是 10^6-10
  • 每個用戶的資料點:可能是 10^2-10
  • 這個函式每秒被呼叫幾次?

不同的 n 範圍對應不同可接受的複雜度:

n可接受的最大複雜度
≤ 10O(n!) 都行
≤ 100O(n³)
≤ 10^4O(n²)
≤ 10^6O(n log n)
≤ 10^8O(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 的關鍵問題。