Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

实战:复杂度反推设计

一句话

给你一组真实约束——数据量、QPS、延迟、内存预算——你能不能反推出"在这个工程里只能用哪种复杂度的算法,然后再筛掉哪些算法,最后只剩一两个候选"。这是工程现场最高频的"看一眼就知道用什么"的反射能力。复杂度分析到这一步才真正进入工程实践。

经验阈值表(单机、~1s 内可跑)

把这张表刻在脑子里。看到 n,第一反应是这张表

n 量级可接受复杂度上限工程直觉
≤ 10任意(甚至 O(n!))全枚举、回溯都能跑
≤ 20O(2ⁿ)状压 DP 的典型规模
≤ 100O(n³)三重循环 / Floyd-Warshall
≤ 1000O(n²)嵌套循环、朴素图算法
≤ 10⁴O(n²) 紧别再上 n²,准备 n log n
≤ 10⁵O(n log n)排序+二分/堆/线段树回退
≤ 10⁶O(n log n) 勉强,O(n) 稳越接近 O(n) 越稳
≥ 10⁷O(n) 上限单机已接近物理极限

tip

这张表的基础是 ~10⁸ ops/sec——现代 CPU 单核的真实工作速率(cache 友好 + 100% 利用率),不是 GHz 标称的 ~10⁹ cycle/sec。~10⁸ 是把"内存墙 + 分支预测 + SIMD 利用率"全算进去后的保守数.

完整反推流程

1. 拿到约束:n / QPS / 延迟 / 内存
2. 用阈值表推断 ≥ 必须的复杂度上限
3. 列出所有"算法 + 数据结构"组合覆盖该上限
4. 逐项筛:常数因子、cache 友好性、可并行性、编码复杂度
5. 给候选实现,跑 benchmark
6. 不达标 → 回到 3 + 加抽象(换 OS 策略、上 SIMD、上 GPU、上 FPGA)

这是"算法 → OS → 硬件"的反推链。第 4 步开始往硬件层探,到第 6 步可能就跳到 GPU/FPGA 重新做抽象。

案例 1:亿条日志中找 Top K 高频词

约束:百亿条日志 × 200 字节 ≈ 2 TB;要求 1s 内出结果。

粗扫

2 TB / 单机 SSD 顺序读 3 GB/s = ~700 s  ⇒ 单机做不完
1 s ⇒ 总数据吞吐至少 2 TB/s ⇒ 必须 sharding

走分布式

每台机器分到 N/M 份,每份做局部 Top K,再用 reduce phase 合并:

  • 单机局部:堆 + 哈希表 → 复杂度 O(N/M · log K).
  • reduce:合并 M 份小 Top K,再做一次 O(M · log K) 排序.

工程注意:

  1. 堆 + 哈希表 而不是 sort:sort 会 O((N/M) log (N/M)),慢一截;
  2. 推荐用 Count-Min Sketch 一次 sweep 取近似 Top K,准确度可接受且 O(N/M);
  3. mapreduce 框架(Hadoop)默认走 sort,open 算法定制时要 override。

到这里反推就跳出了"选什么数据结构"——已经跳到分布式架构(map/reduce)和应用模式(sketch 替代精确)。

案例 2:实时字符串匹配、模式集上万

约束:n ≈ 10⁷ 流式文本、模式集 P ≈ 10⁴、每个模式 ≤ 30 字符、单机、~100 ms / MB.

朴素怀疑 KMP per pattern

O(P · n) = 10⁴ × 10⁷ = 10¹¹ ops ⇒ 不可能 100 ms 跑完.

反推让"沿文本扫描一次" ⇒ Aho-Corasick

复杂度 O(n + Σ|P| + hits):建 Trie 失败链 ~O(Σ|P|) 一次性预编译,文本扫一次。

工程常量角度看:

  • 顺序扫文本,cache miss 极少;
  • Trie 节点 ~字节级别数组(256 槽),cache 容易闪;
  • Java/Go 等都能轻松跑出 1 GB/s 的 AC 扫描率.

结论:选 AC 不是品味,是反推决定的唯一答案.

案例 3:10⁵ QPS 的 LRU 缓存

约束:单次操作 ≤ 10 μs;CPU 单核 ~3 GHz.

10 μs = 30_000 cycles,但每次 cache miss 就 ~200 cycle,10 次 cache miss 就用掉 2 μs. 缓存结构的 ops 数必须压到 30-50 ops,这才是真正可行上限.

候选

方案复杂度实际 ops难点
哈希 + 双链O(1) 摊还~50内存碎片 + 分配
哈希 + 数组 + 世代O(1)~30 但 cap 小适配小上限
Cuckoo hashingO(1) 摊还~30-100rehash 失败处理

工程层的反推

代码上看是 map[k]*Node,但真要做 10⁵ QPS 必须:

  1. 预分配内存池:避免 runtime.malloc 在每 op 里触发;
  2. sharded map:单 map 在高并发下中心化锁(Go runtime 的 hash growth 用全局锁),分 16-64 片即可;
  3. lock-free:考虑用 sync.Map(读多写少)或 craq / lfds 这类 lock-free map;
  4. 不写 undo/redo 日志:内存里 LRU 单次操作不要 WAL,否则延迟穿 latency 上线。

到这里,反推已经从"算法选型"跳到"并发同步策略"

案例 4:分布式严格递增序号

约束:10⁶ req/s、全局严格递增、可用性要求 99.99%.

  • 用 Redis counter:O(1) 但单点瓶颈 ~10⁵/s,挂;
  • 用 atomic single-machine:单机 10⁶/s OK,但全局不行;
  • 用雪花算法(Snowflake):timestamp(41bit) | worker_id(10bit) | seq(12bit),每台机器 O(1) 生成、全局递增;
  • 但雪花不保证严格连续,gap 在时钟碰撞或 worker restart 时会出现.

这就是为什么 10⁶ req/s 真实场景都基本用雪花替代品——并接受小概率 gap。复杂度不是瓶颈,争用+同步才是

案例反推到硬件

考虑一个"每秒 10 GB 视频帧,把每帧 RGB→YUV420 并下采样"的负载:

  • 朴素 C 实现循环:~5 GB/s, 还差一半;
  • 改 SIMD (AVX2-pmulhrsw + packuswb):~30 GB/s;
  • 上 GPU (nvjpeg):~5-10 TB/s;
  • 上 FPGA (ISP pipeline):可直接 streaming 一帧 ~16 ms 不爆 buffer.

你会发现:每往硬件层走一层,抽象相同(RGB → YUV 的逻辑没变),但实现常数提高几个数量级。这是"软件 → 硬件"反推的真实路径:当你纯软件层的常数因子打不进 SLA时,下一步不是优化代码而是换硬件抽象层.

反推陷阱清单

  1. 隐藏 O(n²):循环里隐藏了 string 拼接 "a" + "b" + "c",每次拷贝全串 ⇒ O(n²). 换 StringBuilder / bytes.Buffer / fmt.Fprintf.
  2. map 迭代 "看起来 O(1)":实际 O(n).
  3. 哈希 rehash 尖刺:长尾敏感场景要 reserve 或换 incremental rehash.
  4. 递归深度:Python 默认 1000;Go 1.18+ 默认 1GB 栈,但深度递归仍有 GC + stack copy 代价. 对超深递归改迭代版.
  5. τ-抖动 vs 平均延迟:摊还 O(1) 也救不了硬实时.

练习

  1. 给 10 TB 文本找出 top-10 高频词,单机 SSD,1 小时预算。给方案。
  2. 你接到一个 10⁶ QPS 的 LRU 缓存需求,硬盘只能扛住 10³ QPS;反推架构(一致性/分区/降级)。
  3. 给一段简单的 Python 代码,找出它的复杂度并改造:
    def process(items):
        result = []
        for x in items:
            if x not in result:
                result.append(x)
        return result
    
    • set 而不是 list 做"重复检查": O(n²) → O(n).
    • 但如果"items 含不可哈希对象"必须保留 O(n²) —— 选型仍要看语义.

这一章带走的东西

  • 看到规模第一反应是阈值表;
  • 算法复杂度只解决"形状",工程真实约束推你进 OS/硬件层;
  • 反推的尽头往往不是"换个数据结构",而是换抽象层——SIMD/GPU/FPGA;
  • 摊还 O(1) 在硬实时下不是 O(1),平均 ≠ 最坏.

下一章 → 基本数据结构