mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2220 字
6 分钟
Trie 前缀树(Trie)
2026-06-13

一、为什么需要 Trie#

你在搜索框里输入「sh」,下拉列表立刻弹出「shell」「shift」「short」——这种自动补全体验几乎无处不在。问题是,怎么做到的?

最直觉的方案是哈希表:把所有单词存进去,查找时 O(1) 搞定。但哈希表有个致命缺陷——它只支持精确匹配。想找所有以「sh」开头的词?只能遍历整张表,逐个检查前缀,时间复杂度 O(n)。10 万个词就得扫 10 万次。

排序数组加二分查找呢?先定位到第一个以「sh」开头的词,然后顺序扫描直到前缀不匹配。查找起点是 O(log n),但扫描结果的数量取决于匹配项的多少,最坏情况还是 O(n)。

我们需要一种数据结构,能直接利用前缀的公共部分,一次遍历就定位到所有匹配项。Trie(也叫前缀树、字典树)就是为这个问题而生的。

二、现实类比#

想象一本老式电话簿。书页边缘印着一排字母标签——A、B、C……你翻到「W」那一截,再在 W 区内逐页找「Wang」。所有 W 开头的姓名都集中在同一片区域,不需要翻遍整本书。

Trie 做的事情一模一样:根节点相当于整本电话簿,第一层分支相当于边缘的字母标签,后续每一层都在进一步缩小范围。共享相同前缀的词共享同一条路径,就像「Wang」和「Wu」共享了 W 那一截,但之后各走各的分支。

三、核心思想#

Trie 是一棵多叉树,每条边对应一个字符,从根到某节点的路径拼出的字符串就是该节点代表的前缀。标记为「终止」的节点表示一个完整的单词。

下面是一棵包含 heherhimhisit 五个单词的 Trie:

flowchart TD root(("root")) --> h["h"] root --> i["i"] h --> e["e ★"] h --> i2["i"] e --> r["r ★"] i2 --> m["m ★"] i2 --> s["s ★"] i --> t["t ★"]

其中 ★ 标记的节点是单词终止节点。注意 heher 共享了 h → e 这条路径——e 节点既是 he 的终止节点,又是 her 的中间节点。这就是 Trie 节省空间的关键:公共前缀只存一份

3.1 核心操作与复杂度#

操作时间复杂度说明
查找(Lookup)O(k)k 为目标字符串长度,与树中单词数量无关
插入(Insert)O(k)逐字符沿路径走,不存在则创建节点
前缀搜索(Prefix Search)O(k + m)k 为前缀长度,m 为匹配结果数
删除(Delete)O(k)从叶节点向上回收无分支的空闲节点

空间复杂度最坏情况 O(n × k),其中 n 是单词数量,k 是平均长度。因为每个字符都可能对应一个独立节点,100K 个单词可能产生 500K+ 个节点。但如果单词之间共享大量前缀(比如英文单词),实际节点数远小于最坏值。

3.2 前缀搜索的工作方式#

前缀搜索分两步:先用 O(k) 定位到前缀对应的节点,再从该节点出发做一次子树遍历,收集所有终止节点。第二步的代价取决于匹配结果的数量 m,而不是树中单词的总数 n。这正是 Trie 的核心优势——前缀查询的代价与结果集大小成正比,与全量数据规模无关

四、变体与对比#

4.1 Trie vs 哈希表 vs 排序数组#

特性Trie哈希表排序数组Radix Tree
精确查找O(k)O(1)O(log n)O(k)
前缀搜索O(k + m)O(n) 全表扫描O(log n + m)O(k + m)
插入O(k)O(1) 均摊O(n) 需移动元素O(k)
空间开销较大(节点多)中等较小(紧凑存储)较小(压缩单分支)
有序遍历天然有序无序有序天然有序
最长前缀匹配原生支持不支持需多次二分原生支持

Trie 的优势集中在「前缀操作」上。如果只需要精确查找,哈希表 O(1) 的性能更优;如果数据静态且需要紧凑存储,排序数组更合适。

Radix Tree(基数树) 是标准 Trie 的压缩变体,它把只有单个子节点的链路合并为一个节点。比如 h → e → l → l → o 这条没有分叉的路径,Radix Tree 会直接存成 hello 一个节点,省掉中间 4 个节点的指针开销。在键的公共前缀较长或存在大量单分支路径时,Radix Tree 的节点数通常能减少 5-10 倍,内存占用显著降低。Linux 内核的 IP 路由表和 Nginx 的路由匹配都用的是 Radix Tree。选择建议:如果内存敏感且键的前缀共享度高,优先选 Radix Tree;如果键的分支因子很高(每层多个子节点),标准 Trie 的实现更简单直接。

4.2 压缩 Trie(Radix Tree / Patricia Trie)#

标准 Trie 有个明显的空间浪费:一条链上每个节点只有一个子节点时,整条链其实可以压缩成一个节点。比如 h → e → l → l → o 可以压缩为 hello 一个节点。

Redis 的 rax 就是这样做的——它把单分支链合并,节点数通常能减少 5-10 倍。这种变体叫 Radix Tree 或 Patricia Trie,在内存敏感的场景下是更好的选择。

五、多语言实现#

5.1 Go 实现#

package trie
// TrieNode 表示 Trie 的一个节点
type TrieNode struct {
children map[rune]*TrieNode // 子节点映射
isEnd bool // 是否为单词终止节点
}
// Trie 前缀树
type Trie struct {
root *TrieNode
}
func New() *Trie {
return &Trie{root: &TrieNode{children: make(map[rune]*TrieNode)}}
}
// Insert 插入一个单词,逐字符沿路径走,不存在则创建
func (t *Trie) Insert(word string) {
node := t.root
for _, ch := range word {
if node.children[ch] == nil {
node.children[ch] = &TrieNode{children: make(map[rune]*TrieNode)}
}
node = node.children[ch]
}
node.isEnd = true
}
// Search 精确查找,必须完整匹配且终止节点标记为 true
func (t *Trie) Search(word string) bool {
node := t.findByPrefix(word)
return node != nil && node.isEnd
}
// StartsWith 前缀搜索:返回所有以 prefix 开头的单词
func (t *Trie) StartsWith(prefix string) []string {
node := t.findByPrefix(prefix)
if node == nil {
return nil
}
var results []string
// 从前缀节点出发,深度优先收集所有终止节点
t.collect(node, prefix, &results)
return results
}
// findByPrefix 沿前缀路径定位到目标节点
func (t *Trie) findByPrefix(prefix string) *TrieNode {
node := t.root
for _, ch := range prefix {
if node.children[ch] == nil {
return nil
}
node = node.children[ch]
}
return node
}
// collect 深度优先遍历,收集所有终止节点对应的完整单词
func (t *Trie) collect(node *TrieNode, path string, results *[]string) {
if node.isEnd {
*results = append(*results, path)
}
for ch, child := range node.children {
t.collect(child, path+string(ch), results)
}
}

Go 版本用 map[rune]*TrieNode 存子节点,支持 Unicode 字符。InsertSearch 都是 O(k) 的单路径操作,StartsWith 在定位前缀节点后做一次 DFS 收集结果。

5.2 TypeScript 实现#

// TrieNode 表示 Trie 的一个节点
class TrieNode {
children: Map<string, TrieNode> = new Map();
isEnd: boolean = false;
}
// Trie 前缀树
class Trie {
private root: TrieNode = new TrieNode();
// 插入一个单词
insert(word: string): void {
let node = this.root;
for (const ch of word) {
if (!node.children.has(ch)) {
node.children.set(ch, new TrieNode());
}
node = node.children.get(ch)!;
}
node.isEnd = true;
}
// 精确查找
search(word: string): boolean {
const node = this._findByPrefix(word);
return node !== null && node.isEnd;
}
// 前缀搜索:返回所有以 prefix 开头的单词
startsWith(prefix: string): string[] {
const node = this._findByPrefix(prefix);
if (node === null) return [];
const results: string[] = [];
this._collect(node, prefix, results);
return results;
}
// 沿前缀路径定位到目标节点
private _findByPrefix(prefix: string): TrieNode | null {
let node = this.root;
for (const ch of prefix) {
const child = node.children.get(ch);
if (child === undefined) return null;
node = child;
}
return node;
}
// 深度优先遍历,收集所有终止节点对应的完整单词
private _collect(node: TrieNode, path: string, results: string[]): void {
if (node.isEnd) results.push(path);
for (const [ch, child] of node.children) {
this._collect(child, path + ch, results);
}
}
}

TypeScript 版本用 Map<string, TrieNode> 替代 Go 的 map[rune]*TrieNode,逻辑完全一致。两个实现都遵循同一个核心思路:查找沿路径走,前缀搜索定位后再 DFS

六、生产验证#

项目用途源码位置
Linux KernelIP 路由表的 FIB 最长前缀匹配,使用 LC-trie(Level-Compressed trie)fib_trie.c#L130-L165
RedisStreams 的消息索引与消费者组元数据管理,使用 rax(压缩 Radix Tree)rax.h#L82-L130

Linux Kernelfib_trie.c 实现了 IPv4 路由表的最长前缀匹配。IP 路由需要的是「最长匹配」而非精确匹配——比如目的地址 192.168.1.5 可能同时匹配 192.168.1.0/24 和 192.168.0.0/16,路由器要选更具体的 /24。用哈希表的话,最多需要尝试 32 次不同前缀长度的查找;而 Trie 只需一次遍历,沿途记录最深的匹配节点即可。

Redisrax 是一棵压缩 Radix Tree,用于 Streams 数据结构。每个 Stream 节点内部用 rax 索引消息 ID(时间戳+序号),消费者组也用 rax 管理待确认消息列表。相比标准 Trie,rax 把单分支链压缩为单个节点,内存占用大幅降低。

七、小结#

何时使用#

  • 自动补全 / 输入联想:搜索引擎、代码编辑器、命令行补全——Trie 的前缀搜索天然适配
  • IP 路由最长前缀匹配:一次遍历即可找到最具体的路由条目,比哈希表多次查找高效
  • 拼写检查:快速判断一个词是否在词典中,或找出编辑距离为 1 的候选词
  • 去重与排序:Trie 天然按字典序组织,遍历即有序输出,同时自动去重

何时不用#

  • 只需精确查找:哈希表 O(1) 更快,Trie 的 O(k) 没有优势
  • 键是纯数值:数值型键更适合用二叉搜索树或跳表,Trie 按字符拆分反而浪费
  • 内存受限且键稀疏:键之间几乎没有公共前缀时,Trie 的节点数接近 n × k,空间开销远超哈希表
  • 键很短且唯一性高:短键意味着每层分支少、共享前缀少,Trie 的空间优势无法发挥

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Trie 前缀树(Trie)
https://blog.souloss.com/posts/programming/data-structures/data-structures-trie/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时