你正在给手机做一个中文输入法。词库里有 n 个词,用户在键盘上敲了一串英文字母 s,你要立刻回答两件事:
问题 A:s 是不是一个完整的词?(单词查找)
问题 B:词库里有多少个词以 s 为前缀?(前缀统计,用于给候选词排序)
你第一反应是用哈希表:把所有单词存进 unordered_set,问题 A 用 O(L) 解决,很快。可是问题 B 呢?你只能把 n 个单词全部过一遍,逐个判断前缀——每次查询 O(nL),用户每敲一个字母就要卡一下。
你想了想:"要不我把所有前缀都存进哈希表?"于是对每个单词,把它所有的前缀都插一遍。插入一个长度 L 的单词要做 L 次,总插入代价 O(ΣL),勉强能接受。但内存爆炸了——c、ca、cat、cate… 每个前缀都单独占一个字符串,前缀并没有被共享。
所有单词共享着大量的公共前缀,而哈希表把这份"公共"彻底浪费了。
你的同事在白板上写下几个词:cat、car、cow、dog,然后从根开始,一位一位地往下画:c 往下分叉出 a 和 o,a 再分叉出 t 和 r…
「你看,cat 和 car 共享了 ca 这一段路。找 cat 就是从根出发,沿着 c→a→t 走三级楼梯。走不动了,说明词不存在;能走完并且终点有标记,说明词存在。」
「至于前缀有多少个词?在 ca 这个路口记一个数就行——路过它一次就加一。查询 ca 就是走到 ca 这个节点,读出那个数字。」
你盯着这张图:这不就是把字符串的"前缀"从隐藏的冗余,变成了显式的树形结构吗?这棵树的每一层对应一个字符位置,每条从根出发的路径对应一个前缀,每个节点对应一个"前缀类"。
你给它起了个名字——字典树(Trie),也叫前缀树(Prefix Tree)。因为从根到某个节点走下来,恰好拼出一个前缀;树的分叉数就是字符表的大小。
💡 故事背后的算法思想
① 用"树的层"表示"字符串的位置"。字典树是一棵有根树,每条边标注一个字符。深度为 d 的节点代表一个长度为 d 的字符串。父子关系就是"前缀 + 一个字符"的关系。
② 共享前缀 = 共享路径。这是字典树最本质的贡献。所有具有相同前缀的字符串,在树中共享同一条从根出发的路径。n 个单词的字典树规模是 O(总字符数),而不是 O(n × 平均长度) 的重复存储。
③ 查询复杂度只跟串长有关。插入、查找、前缀查询都是 O(L),与字典里有多少个单词完全无关。这是它的杀手级优势:查询代价可预测、无哈希冲突、无最坏退化。
④ 节点上的计数是全部魔法。用一个 cnt[u] 记录"有多少个词以 u 对应的字符串为前缀",用 lst[u] 标记"u 处是否是一个完整单词的结尾"。有了这两个量,绝大多数 Trie 题目都能做。
⑤ 把字符表换成二进制位,就是 01-Trie。这是竞赛中最重要的延伸:每个数从高位到低位走一个 0/1 分支,于是"求最大异或对""区间异或最值""可持久化 01-Trie"全都能做。01-Trie 是很多异或问题的唯一优雅解法。