引言
众所周知,字典的用途无非就是用来查找字的。顾名思义,字典树自然也是起到查找的作用。我们可以看看以下几个问题:
利用 map 存储每个单词,调用 count 即可得出结果
将单词拆分后,用 map存储每一段,调用 count 得出结果
若 n <= 2e5 时,如果继续使用map,将会 TLE,这时我们就需要使用一种高级的数据结构,即 Trie树
基本概念及性质
Tire树,又称字典树。
- 是一种高效存储查找字符串集合的数据结构
- 核心思想是利用空间换时间,利用字符串的公共前缀来降低查询时间的开销
- 适用于 全是同类字符的情形:
- 小写字母
- 大写字母
- 数字
- 0 / 1
下标是 0 的节点 (头节点),即是根节点,又是空节点
树的每个节点包含的子节点字符都不相同
从根节点到某一节点,路径上所经过的字符拼接起来,就是该节点对应的字符串
实现思路及图解
int son[N][26];// 后面的 26 数字视题目而定
int idx;
// son[][]存储树中每个节点的子节点
// idx 记录每个字符的编号
:
- 从前往后依次遍历每个字符,看是否有此字母作为子节点,没有则创建新的节点
- 在每个单词结尾打上标记,表示以当前字母结尾的单词存在
插入操作
对于每个字符,我们给它指定一个插入位置,即给每个字母贴上编号
如 数组 ,表示 的节点的,孩子, 是 的节点
- 说白了就是,
如图,当我们依次插入 abcd,abd,ace,bcd ,abda 时,我们将得到第一种编号,表示编号结果。因为先输入的是 abcd,所以 a ,b,c,d的分别是 1,2,3,然后输入的是abd,因为 a,b 此前已经输入,即是公共前缀,故从 d 开始编号,所以 d 是 4,此后输入以此类推
可以发现相同的字母的编号可能不同
如图,当我们依次插入 abcd,abd,ace,bcd ,abda 时,得到第一种编号的同时,我们可以得到第二种编号,表示编号结果。因为每个节点最多有26个子节点,故此我们可以按他们的字典序从0 ~ 25编号,也就是。因为先输入的是 abcd,所以 a ,b,c,d的分别是 0,1,2,3,此后输入以此类推
可以发现相同字母的编号相同
代码模板
void insert(char *str)
{
int p = 0; // 从根节点出发,根节点的编号为 0
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';// 第二种编号
if(!son[p][u]) // 如果之前没有从 p 到 u 的前缀
son[p][u] = ++idx; // idx 即为第一种编号
p = son[p][u]; // 顺着字典树往下走
}
}
查找操作
(1)
对于一个给定的字符串,我们从左往右扫描每个字母,顺着字典树往下找
- 若能走得到这个字母,往下走,当字符串扫完后,返回此
- 若能走不到这个字母,结束查找,即没有这个前缀, 返回
我们可以使用 记录以当前字母结尾的单词次数 ,插入完毕后,我们使 数组 加 1,表示以当前字母结尾的单词数个数加 1
如下图所示,依次插入单词后,我们用 表示以该字母结尾的单词出现的次数
代码模板为
int cnt[N]; // cnt[]存储每个节点结尾的单词结尾,即打标记
void insert(char *str)
{
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if(!son[p][u])
son[p][u] = ++idx;
p = son[p][u];
}
cnt[p] ++; // 表示当前单词数 加 1
}
int query(char *str)//查找以某个单词出现的次数
{
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if(!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
例如,当我们查找 ac 这个单词出现的次数时
对于,有
0 | 1 | 2 | |
---|---|---|---|
'a' | 'c' | ’\0‘ |
有
因为此时
故 (根节点 的编号为 0,它的第 0 个 孩子编号为 1)
故 如下图所示,走到编号为 1 的 a 处
有
因为此时
故 (a 的编号为 1,它的第 2 个 孩子编号为 6)
故 如下图所示,走到编号为 6 的 c 处
最后 ,我们即可得到单词 ac 的数量为 3
(2)
对于一个给定的前缀字符串,我们从左往右扫描每个字母,顺着字典树往下找
- 若能走得到这个字母,往下走,当字符串扫完后,返回此
- 若能走不到这个字母,结束查找,即没有这个前缀, 返回
我们可以使用 记录以此前缀出现的次数 ,插入过程中,我们使 数组 加 1,表示此前缀的某段部分的出现的次数加 1
依次插入单词后,我们用 表示此单词出现的次数,
相应地,我们可以得到各个前缀段出现的次数,我们用 表示以该前缀段出现(从 该字母 到 根节点 所经过的字母)的次数,如下图所示
代码模板为
int sum[N];// sum[] 记录某段前缀出现的次数
void insert(char *str)
{
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if(!son[p][u])
son[p][u] = ++idx;
sum[son[p][u]] ++;//表示此前缀某段部分出现次数加 1
p = son[p][u];
}
}
int find(char *str)//查询某个前缀出现次数
{
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if(!son[p][u]) return 0;
p = son[p][u];
}
return sum[p];
}