字典树 (Trie) 入门

1. 什么是字典树?

想象一下你查英语字典。
当你要查单词 "apple" 时,你不会把字典里几万个词从头看一遍。你会:

先翻到 a 开头的区域;
在 a 里面找到 p;
再往下找 p... 直到找到 l 和 e。
字典树(Trie) 就是把这个过程具象化的一种数据结构。

它的样子:一棵多叉树。
它的边:每条边代表一个字符。
它的节点:节点本身不存字符,节点代表“走到这里组成的字符串”。
它的核心优势:高效解决“前缀”问题。比如查“有多少个单词是以 app 开头的”,Trie 比哈希表强得多。

2. 核心原理

我们要怎么用树存下单词"abc",“abb”,“bca”,"bc"呢?见图

根节点:是空的(起点)。
第一层:只有两条边 a , 'b',通向一个节点(代表前缀 "a" 和 "b")(所有我们要存储的单词都以这两个字母开头)。
第二层:从 "a,b" 节点出发,有两条边 a -> b ,'"b -> c',通向新节点(代表前缀 "ab" 和 "bc")。
第三层(分叉了!):
从 "bc" 节点出发,有一条边 a -> 结束(代表 "bca")。
从 "ab" 节点出发,有一条边 b -> 结束(代表 "abb")。
有一条边 ab -> 再接 c -> 结束(代表 "abc")。
发现了吗? abb, abc, 的前两个字母 ab 在树上是共用的。这就是 Trie 节省空间和查询快的原因。

3. 代码实现(静态数组版)

最常用的是静态数组模拟,比指针版好写且不易出错。

3.1 变量定义

const int N = 100005; // 预计所有单词的总长度之和

// son[p][u] 表示:节点 p 的第 u 个子节点是谁
// u 代表字符,比如 'a'是0, 'b'是1 ... 'z'是25
int son[N][26]; 

// cnt[p] 表示:以节点 p 结尾的单词有多少个
int cnt[N]; 

// idx 表示:当前用到了第几个节点(类似身份证号分配器)
int idx; 

3.2 插入操作 (Insert)

逻辑:顺着树往下爬,路通就走,路不通就修路(新建节点)。

// 插入一个字符串 s
void insert(string s) {
    int p = 0; // p 是指针,从根节点(0)开始
    for (int i = 0; i < s.size(); i++) {
        int u = s[i] - 'a'; // 将字符 'a'-'z' 映射为数字 0-25
  
        // 如果当前节点 p 没有对应字符 u 的路,就新建一个节点
        if (!son[p][u]) son[p][u] = ++idx;
  
        // 走到下一个节点
        p = son[p][u]; 
    }
    // 循环结束,p 停在了单词的最后一个字符节点上
    cnt[p]++; // 标记一下:这里是一个单词的结尾
}

3.3 查询操作 (Query)

逻辑:顺着树往下爬,如果在中间断了,说明单词不存在。

// 查询字符串 s 出现了多少次
int query(string s) {
    int p = 0;
    for (int i = 0; i < s.size(); i++) {
        int u = s[i] - 'a';
        // 如果路断了,说明没有这个单词
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    // 走完了,返回记录的次数
    return cnt[p];
}

4. 只有这两个操作吗?(进阶一点点)

Trie 的强大之处在于修改 cnt 数组的含义。

场景 A:找前缀 (Prefix Count)
题目:给你一堆单词,问有多少个是以 "app" 开头的?
修改:

在 insert 里的循环内部,每次 p = son[p][u] 之后,都执行 cnt[p]++。
这表示:有一个单词经过了节点 p。
查询时走到 "app" 的末尾节点,直接返回那个节点的 cnt 即可。
场景 B:判断是否存在 (Boolean)
题目:问单词是否存在?
修改:

cnt 不需要是 int,可以是 bool is_end[N]。
结尾时 is_end[p] = true。

5. 初学者必坑指南

空间开多大?

数组 son 的大小 N 不是单词个数,而是所有单词长度的总和。
例如:104个单词,每个长10,N 就要开 105
小心内存爆炸:int son[100000][26] 约占 10MB,如果是 long long 或 N 再大点就要注意了。
多组数据初始化

如果题目有多组测试数据,不要用 memset(son, 0, sizeof(son))!这会超时。
正确做法:把 son 和 cnt 数组里用过的部分清零,或者简单的把 idx = 0,并且在 insert 时如果遇到旧数据覆盖掉(稍微复杂点,初学者建议老老实实写循环清空 0 到 idx 的范围)。
字符映射

如果题目包含大写字母或数字,u = s[i] - 'a' 就不够用了。
可能需要写个小函数 int get_id(char c) 把字符映射到 0-62 之间。

posted @ 2026-04-11 09:14  PLJZ  阅读(3)  评论(0)    收藏  举报