这一周学的是数据结构选型。
同样的“字符串匹配”,有KMP、Trie、Rabin-Karp三条路;同样的“区间求和”,有前缀和、差分、树状数组、线段树四种结构。
它们不是“谁替代谁”的关系,而是各自在不同约束下最优:
Trie 赢在「前缀」 —— 查前缀O(L),与词表规模无关 哈希 赢在「O(1) 比对」 —— 串相等判定从O(L) 降到O(1),代价是概率性碰撞 树状数组 赢在「常数小」 —— 十几行代码、一个数组,够用时的最优解 线段树 赢在「通用」 —— 任意结合律聚合 + 区间修改,代价是4n空间没有银弹,只有取舍。而“知道什么时候该选哪个”,正是这一周要给你的东西。
📊 本周六题结构对照表
| 结构/算法 | 核心题目 | 时间复杂度 | 空间 | 一句话核心 |
|---|---|---|---|---|
| Trie前缀树 | LC.208 / LC.211 | 插入·查询O(L) | O(总字符数) | 公共前缀共享路径,前缀检索与词表规模无关 |
| 异或字典树 | LC.421 | O(n)(32n) | O(32n) | 二进制Trie + 高位优先贪心 |
| 字符串哈希 | LC.187 / LC.1044 | O(n)(滚动) O(nlogn)(二分) | O(n) | 多项式指纹 + O(1)滚动递推 |
| Manacher | LC.5 | O(n) | O(n) | 分隔符统一奇偶 + 镜像复用半径 |
| 树状数组 | LC.307 | 更新·查询O(logn) | O(n) | lowbit分块,前缀和的可修改版 |
| 线段树 | LC.307(再解) | 更新·查询O(logn) | O(4n) | 区间分治 + 懒标记,任意结合律聚合 |
一条隐藏主线:这六题里有四题都靠“复用已算过的信息”——Manacher复用镜像半径、Trie复用公共前缀、线段树复用整块区间、滚动哈希复用上一个窗口的指纹。
这条主线和KMP的next数组是同一条:已算过的信息,绝不再算第二遍。
🌳 字符串算法家族树
字符串问题 │ ┌─────────────────┼─────────────────┐ 单模式匹配 多模式/前缀 回文 │ │ │ ┌────┴────┐ ┌─────┴─────┐ │ KMP Rabin Trie AC 自动机 Manacher -Karp| 算法 | 解决什么 | 复杂度 | 确定性 | 关键特性 |
|---|---|---|---|---|
| KMP | 单模式串匹配 | O(n+m) | ✅ 确定 | 失配时主串指针不回退 |
| Rabin-Karp | 单模式 /重复子串 | 期望O(n),最坏O(nm) | ❌ 有碰撞 | 指纹比对 + 滚动,双哈希兜底 |
| Trie | 前缀检索/ 自动补全 | O(L) | ✅ 确定 | 与词表规模无关 |
| AC 自动机 | 多模式匹配 | O(n + 总词长) | ✅ 确定 | Trie + KMP失败指针 |
| Manacher | 最长回文子串 | O(n) | ✅ 确定 | 分隔符统一奇偶 + 镜像复用 |
怎么选?四句话:
- 找一个模式串 →KMP(确定性最优)或Rabin-Karp(更好写)
- 找一堆模式串 / 查前缀→Trie;要一次扫完文本命中所有模式 →AC自动机
- 找回文→ 中心扩散(够用)或Manacher(加分)
- 找重复子串 / 最长公共子串→滚动哈希 + 二分答案
📋 区间查询数据结构选型表(本篇最重要的一张表)
选型时只问三个问题:
① 数据变不变?
② 查的是区间还是前缀/最值?
③ 要不要区间修改?
| 场景 | 该用什么 | 查询 | 更新 |
|---|---|---|---|
| 数组静态,查区间和 | 前缀和 | O(1) | — |
| 数组静态,做区间批量加减 | 差分数组 | O(n)还原 | O(1) |
| 数组可变,区间和,单点改 | 树状数组(或线段树) | O(logn) | O(logn) |
| 数组可变,区间和,区间改 | 线段树 + 懒标记 | O(logn) | O(logn) |
| 数组可变,区间最值 | 线段树 | O(logn) | O(logn) |
| 滑动窗口最值 | 单调队列 | O(1)均摊 | O(1)均摊 |
三个决策点讲透:
- 静态 vs 可变:静态就别上重型结构——前缀和O(1)查询是最优解
- 能否用减法拆分:区间和 = 前缀差,树状数组够用;区间最值不能用减法撤销,必须上线段树
- 单点改 vs 区间改:区间改只有线段树 + 懒标记能 O(logn)
一句话口诀:
静态查和用前缀,批量加减用差分;可变查和用树状,要最值或整段改才上线段。
📊 本周实测数据一览
| 对比场景 | 赢家 | 实测结果 |
|---|---|---|
| 10万词表、1000次前缀查询 | Trie | 0.0003s vs 全表扫描 1.56s(5682×) |
| 10万单词建树空间 | Trie | 节点数比总字符数少44.7% |
| n=2万求最大异或 | 逐位 HashSet | 0.151s vs Trie 0.392s(Trie慢2.6×,但同量级) |
| 同上,暴力 O(n²) | — | n=4000 已 2.77s,n=2 万外推 ≈ 69s |
| DNA 20万、窗口 L=10 | 切片 | 0.057s vs 滚动哈希 0.078s(切片快 1.4×) |
| DNA 20万、窗口 L=1000 | 滚动哈希 | 0.070s vs 切片0.190s(快 2.7×) |
| 随机串最长回文 | 中心扩散 | 0.0018s vs Manacher 0.0027s |
全a串(回文极长) | Manacher | 0.0023s vs 0.7293s(314×) |
| n=10万、10万次单点改+区间和 | 树状数组 | 0.307s vs 线段树 1.061s(3.5×) |
| n=10万、2万次区间加+区间和 | 线段树+懒标记 | 0.937s vs 朴素外推 ≈ 97.7s(104×) |
三条比“谁更快”更重要的教训:
- 量级碾压常数:Trie比HashSet慢 2.6倍看着很惨,但两者都是 O(n),而暴力 O(n²)差的是25倍。先解决量级,再抠常数。
- 平均快的,最坏可能崩:中心扩散在随机串上赢1.5倍,在全
a串上输314倍。生产环境要防的是退化输入。 - 常数会翻转结论:同样是O(n),短窗口下切片比滚动哈希快。复杂度表决定上限,常数决定你在常规数据上到底赢不赢。
🎯 再加一条心法
今天补上最后一条——它正是这一周的结晶:
心法:数据结构选型先问三件事——数据变不变、查的是区间还是前缀、要不要整段修改
前八条心法解决的是“这道题怎么想”,第九条解决的是“这道题该装进什么结构”。
很多人一看到“区间求和”就写线段树,却没意识到数组是静态的,前缀和 O(1) 就能拿下;也有人死守前缀和,遇到数组可变就卡住。选错结构,代码写得再漂亮也是绕远路。
判断顺序固定为三步:先问变不变 → 再问查什么 → 最后问改不改区间。三步走完,答案基本就落在上面那张表里了。
九条心法速览
| # | 心法 |
|---|---|
| 1 | 先写暴力再谈优化 |
| 2 | 找单调性 |
| 3 | 空间换时间 |
| 4 | DP 的灵魂是状态设计 |
| 5 | 画表格是理解算法最快的方式 |
| 6 | 面试要“讲出来” |
| 7 | 求方案用回溯,求最值用 DP,能证明无后效性才用贪心 |
| 8 | 贪心不是“感觉对”,是“能证明” |
| 9 | 选型先问:变不变 / 查什么 / 改不改区间 |
📊 全周总结表
| 主题 | 核心题目 | 一句话收获 |
|---|---|---|
| Trie前缀树 | LC.208 / LC.211 | 公共前缀共享路径;search=startsWith+ 检查isEnd |
| 异或字典树 | LC.421 | 二进制Trie + 高位优先贪心,O(n)拿下最大异或 |
| 字符串哈希 | LC.187 / LC.1044 | 指纹 + 滚动递推;碰撞靠双哈希兜底 |
| Manacher | LC.5 | 分隔符统一奇偶 + 镜像复用半径,O(n)求最长回文 |
| 树状数组 | LC.307 | lowbit分块,前缀和的可修改版升级 |
| 线段树 | LC.307(再解) | 区间分治 + 懒标记,通用性换4n空间 |
| 收官总结 | 本篇 | 字符串家族树 + 区间选型表 + 心法第九条 |
每天一个算法,面试不再心虚。