Theory · Data Structure · Trie / Prefix Tree

Trie 字典树

把"共享前缀"变成结构:O(L) 与元素个数无关 · 前缀查询专属 · 从搜索提示到 IP 路由

结构

多叉树,边即字符,从根到节点路径拼出 key——前缀天然聚合

复杂度

插入/查找/前缀全是 O(L),L 为串长——与词典规模 n 无关

落地

搜索提示词、敏感词过滤(AC 自动机)、IP 路由最长前缀匹配

定位:Trie 是"为前缀而生"的结构——哈希查完整 key O(1),但查"所有 a 开头的词"无能为力。三条主线:结构、O(L) 复杂度、前缀类应用。

Prefix Sharing · Visualized

Trie:从根到节点的路径就是 key

存储 app apple api apply 的 Trie 结构 根节点下依次是 a、p、p 三个节点构成共享前缀,第二个 p 分出 i(api 的词尾)和 l,l 再分出 e 与 y 两个词尾,分别对应 apple 与 apply;词尾节点用蓝色标记。 root a p p i l e y apple 与 apply 共享 a·p·p·l 全部路径 WORDS STORED app · apple api · apply 16 个字符 → 7 个节点 (不含 root) 共享前缀 = 结构性去重 蓝色 = 词尾(isEnd) app、api 中途成词
看图说话三件事:边即字符、路径即 key、共享前缀即共享子树。16 个字符只花 7 个节点——"结构性去重"是 Trie 的空间本质。isEnd 标记解决"app 是 apple 前缀"的包含关系。

Implementation · Go

Go 实现:三段式 API 一百行内

type Trie struct {
    children [26]*Trie // 或 map[byte]*Trie
    isEnd    bool
}
// 插入 O(L)
func (t *Trie) Insert(w string) {
    node := t
    for i := 0; i < len(w); i++ {
        c := w[i] - 'a'
        if node.children[c] == nil {
            node.children[c] = &Trie{}
        }
        node = node.children[c]
    }
    node.isEnd = true
}
// 查找 O(L):走完还要验 isEnd
func (t *Trie) Search(w string) bool {
    n := t.walk(w)
    return n != nil && n.isEnd
}
// 前缀 O(L):走完即是答案
func (t *Trie) StartsWith(p string) bool {
    return t.walk(p) != nil
}

children:数组还是 map?

小写字母集 26 定长 → 数组最快(下标直达、无哈希开销);字符集大/含中文 → map[byte]*Trie 或排序切片。工程折中:数组 + 惰性建节点。

Search 与 StartsWith 的差别只在一步

走完路径后,Search 要检查 isEnd("app" 在树里不代表 "ap" 是完整词);StartsWith 只要路径存在就是 true。

删除

自底向上递归:词尾去 isEnd,若节点无孩子且非其他词尾则逐层摘除;或用引用计数(路径被多词共享时不能真删)。

复杂度

插入/查找/前缀全部 O(L)——与词典规模无关。空间最坏 O(26·总字符),共享前缀多时远小于此。

代码三 API 必须能默写;两个高频追问:数组 vs map 的选择依据、Search 与 StartsWith 的 isEnd 差别。删除的"共享路径不能真删"是进阶追问点。

Where Tries Live

Trie 的四块自留地:都和"前缀"有关

① 搜索引擎提示词 / 自动补全

输入 "app" → 沿 Trie 走到节点,收集其子树所有 isEnd 词,按热度排序返回——前缀查询是 Trie 的天赋,哈希表做不了。

② 敏感词过滤(AC 自动机)

把所有敏感词建入 Trie,加上失配指针就是 AC 自动机(多模式 KMP):一次扫描文本同时匹配全部敏感词,O(文本长 + 命中数)。DFA 的工业标准做法。

③ IP 路由最长前缀匹配

路由表按 IP 的二进制位建 Trie(位级分支),查找时记录沿途最深的"有路由"节点——这就是"最长前缀匹配"(LPM)。Linux 内核路由查找的 LC-Trie 同理。

④ 拼写检查 / 词频统计

词典建 Trie + 编辑距离容错走分支(模糊匹配);节点挂计数即词频统计——结构与统计一体。

场景Trie 提供的能力
输入提示词前缀子树枚举 + 热度排序
敏感词过滤多模式一次扫描(AC)
IP 路由 LPM位级前缀 + 最深命中
单词游戏 / 棋盘搜索DFS 时同步走 Trie 剪枝(LC 212)
异或极值01-Trie 贪心走相反位(LC 421)
识别信号:题目出现"前缀"、"多个模式串"、"逐字符匹配"三个词之一,条件反射想 Trie。异或极值(421)是隐藏用法——把数字按二进制位建 01-Trie。
四块自留地 + 五行场景表。AC 自动机和 01-Trie 是两个"进阶变体",面试能主动提到就赢了大多数候选人。底部信号词清单是选题雷达。

Variants · Trade-offs

Trie 家族变体与三大结构对比

压缩 Trie / Radix 树

单孩子链压缩成一条边("app" 整段一条边),节点数从 O(字符) 降到 O(key 数)。Linux 内核的 radix tree(现 XArray)用页缓存索引、Patricia / crit-bit 变体用于路由。

三向搜索树(Ternary Search Tree)

每个节点三叉(小于/等于/大于),兼顾 BST 的省内存与 Trie 的前缀能力——C 实现经典,适合超大字符集。

双数组 Trie(Double-Array Trie)

用两个整数数组表达整棵树,无指针、缓存极佳——中文分词库(如 jieba 变体)的最爱。

AC 自动机 = Trie + 失配

在 Trie 上预构建 fail 指针(指向最长真后缀节点),把"匹配失败回退"变成 O(1) 跳转——KMP 思想在多模式上的推广。

维度Trie哈希表平衡树
精确查找O(L)O(1) 平均O(log n)
前缀查询✓ 天赋✗ 需全扫✗ 需遍历
有序输出✓ DFS 字典序✓ 中序
空间字符集 × 指针(贵)O(n)O(n)
复杂度依赖与 n 无关散列质量平衡性
选型口诀:"查完整 key 用哈希;要前缀有序枚举用 Trie/树;内存紧张的前缀需求上压缩变体(radix/双数组)。Trie 的贵在空间、赢在前缀。"
变体四件套按工业重要性排:radix(内核)、双数组(分词)、三向(字符集大)、AC(安全)。对比表的"复杂度依赖"行很有信息量:Trie 的 O(L) 是无条件保证。

LeetCode Shortlist

必刷题单:Trie 的四种玩法

玩法题目(编号 · 难度)要点
裸实现208 实现 Trie M · 677 键值映射 M · 648 单词替换 M208 是模板题;648 最长前缀替换
通配/模糊211 添加与搜索单词 M · 425 单词方块 H211 的 '.' 要 DFS 分支
矩阵 DFS 协同212 单词搜索 II H · 421 数组两数最大异或 M212 = Trie + 回溯剪枝;421 = 01-Trie 贪心
前缀统计1804 实现前缀树 II M · 面试题:敏感词/提示词设计节点挂 pass/end 计数
刷法建议:208 默写 → 211 练带通配的分支走法 → 212 上强度(Trie + 回溯,双结构协同的代表题)→ 421 打开"01-Trie"脑洞。Trie 题的核心创造力在"把什么建成 Trie":字符串、二进制位、甚至坐标。
四种玩法递进:裸实现 → 通配分支 → 双结构协同 → 01-Trie。第 421 题是"数字也能建 Trie"的思维拓展,面试亮眼。

Interview QA · Part 1

高频追问:原理与复杂度

1 · Trie 为什么是 O(L) 且与 n 无关?

路径即 key

查找 = 沿 key 的每个字符走一步,步数只由 key 长度决定;不需要比较其他元素、也不依赖树平衡——L 固定时是严格常数(对比平衡树的 O(log n·L))。

2 · children 用数组还是 map?

26 数组最快

字符集小且固定(26 字母)→ 数组:下标直达、缓存友好;字符集大/Unicode → map 或排序切片+二分。数组版每个节点固定 26 指针 = 208B,词少时浪费明显。

3 · Trie 和哈希表怎么选?

前缀分水岭

只查完整 key → 哈希 O(1) 更省更快;要前缀查询、有序枚举、逐字符容错 → Trie。Trie 的 O(L) 无随机性假设,哈希是平均 O(1)。

4 · 什么是压缩 Trie(Radix 树)?

单链压边

把"单孩子链"合并成一条带字符串的边,节点数从 O(字符数) 降到 O(key 数)。Linux 内核页缓存索引(radix tree / XArray)与路由 LC-Trie 都是变体。

5 · IP 路由的"最长前缀匹配"怎么实现?

位级 Trie

路由条目按 IP 二进制位建 Trie,查找 32 位过程中记录最深的有效前缀节点——走到头或断链时,最后记录的即最长匹配。这就是路由器 LPM 的标准结构。

6 · AC 自动机和 Trie 什么关系?

Trie + fail 指针

AC = Trie 上加失配指针(指向当前串的最长真后缀节点),匹配失败不回退文本指针而是跳 fail——把 KMP 的单模式思想推广到多模式,一次扫描匹配全部敏感词。

7 · Trie 支持删除吗?

自底向上

支持:词尾清 isEnd,然后自底向上——节点无孩子且不是其他词尾则摘除;路径被共享(前缀包含)时只能删到安全深度。工程常用引用计数避免递归判断。

8 · Trie 的空间为什么贵?怎么省?

26 指针/节点

数组版每节点固定 26×8B=208B 指针,稀疏浪费严重。省法:① map children 按需建;② 压缩成 radix(边带串);③ 双数组 Trie(纯整数数组);④ 三向搜索树。

前八题覆盖:O(L) 本质、数组 vs map、与哈希对比、压缩变体、LPM、AC 自动机、删除、空间优化。第 5/6 题是超越"背模板"的系统级应用,能答出来直接拉开档次。

Interview QA · Part 2

高频追问:实战与设计

9 · LC 212 单词搜索 II 为什么必须 Trie?

DFS 同步走树

棋盘每个格子向四方向 DFS,同时把"当前路径"在 Trie 里前移:路径不是任何词前缀时整枝剪掉。没有 Trie 就是每个词独立搜索 O(W·m·n·4³),Trie 把共享前缀的重复比较全砍掉。

10 · 421 最大异或值怎么和 Trie 扯上关系?

01-Trie 贪心

把每个数的 32 位二进制从高位到低位建入 01-Trie;查询 x 时从高位起贪心走"相反位"(异或想拿 1)——高位优先保证贪心正确。区间版(XOR 小于 K)在 Trie 上做计数 DP。

11 · 设计"搜索提示词"系统怎么答?

Trie + 热度

① Trie 存查询串、节点挂 TopK 热词小顶堆(或按热度排序的子节点);② 用户输入沿 Trie 前移,到节点直接返回该节点 TopK;③ 热度更新用 dict 定位 + Trie 重插(或定时重建)。是 LC 642 的标准思路。

12 · 敏感词过滤为什么不用正则/哈希?

多模式一次扫

正则交替匹配多词会回溯爆炸;哈希只能整词匹配、无法处理文本内切分。AC 自动机一次 O(文本长) 扫描匹配全部词,无回溯——工业敏感词过滤的事实标准(DFA)。

13 · Trie 能存中文吗?

字符集决定实现

能——children 换成 map[rune]*Trie 或按 UTF-8 字节建(字节级 Trie 字符集只有 256,数组也可行)。分词库普遍用双数组 Trie 存中文词典。

14 · 什么情况下 Trie 不划算?

前缀少=白建

① key 之间几乎无公共前缀(如随机 UUID)→ 退化成"每词一条链",空间浪费、还慢于哈希;② 只做点查;③ 词表静态且内存紧张 → 排序数组 + 二分前缀反而最优。Trie 的收益与前缀共享度成正比。

后六题偏实战:212 的协同剪枝、421 的 01-Trie、两个系统设计题(提示词/敏感词)、中文支持、以及"什么时候不划算"的清醒剂——第 14 题的"前缀共享度"是选型本质。

Related & References

相关知识点与参考

数据结构系列(本分类)

二叉树 →(多叉树的根:树的基本概念)
哈希表 →(点查场景的对照物)
平衡树 →(有序输出的另一种来源)

算法系列

回溯 →(LC 212 的另一半:棋盘 DFS)
复杂度分析 →(O(L) 与 n 无关的含义)
栈与队列 →(BFS 层序枚举子树词)

参考来源(本 deck 结论可溯源至下列一手材料)

E. Fredkin · Trie Memory (1960) / de la Briandais (1959)Trie 的原始论文与词源(retrieval)
Aho & Corasick (1975)AC 自动机:Trie + 失配函数的多模式匹配
Linux kernel lib/xarray.c · net/ipv4(LC-Trie)radix/XArray 页缓存索引与路由最长前缀匹配
LeetCode 208/211/212/421/648/677 官方题解裸实现、通配、矩阵协同、01-Trie
oi-wiki.org/string/trie01-Trie 与 01-字典树进阶(中文对照)
收尾:Trie 与二叉树/哈希/回溯三向链接。出处两篇原始论文(Fredkin 1960、Aho-Corasick 1975)+ Linux 内核落地。总页数 9。