1 基本思想
Trie 单词查找树(Trie 的发音是Try)。用于高效地存储和查找字符串,使用集合数据结构,其存储数据在形式上看起来类似树。
我们可以观看下面的视频来迅速理解其操作原理和流程:
算法演示视频
通过视频,我们可以发现其核心操作是插入操作(即存储数据)与查找操作。
2 核心操作分解
在讲解核心操作之前,我们先学习一下实现Trie单词查找树所使用的数据结构。
2.1 数据结构
我首先直接给出数据结构,而后再逐一讲解,如下:
int son[N][x],cnt[N],idx;
son[N][X]
与idx
N代表了最大字符串长度,X代表了字符种类总数,这个字符的种类总数题目往往会直接告诉我们,所以在实际应用中我们会直接把X处写死为一个常数。
我们存储的基本思路就是将节点使用idx进行编号,一个idx代表一个节点,根节点的idx为0。对于每个节点我们都再使用一个二维数组来维护其所有的子节点信息。
如son[1][1]
,代表结点1的1号子节点。假设idx=1对应的节点为字母a,那么在下图中,son[1][1]
就代表了字母b(索引从0开始)。
由于Trie单词查找树所涉及的题目往往只需处理有限种类的大写或小写字母,以及数字串。据此,我们完全可以使用一个二维数组来维护所有节点的子节点。很奇妙,很优雅。cnt[N]
在之前的操作视频中,我们确定曾经存储过这个字符串,是基于对应节点是不是蓝色,而在代码中我们怎么来对应表示呢?当然是我们的惯用思路,使用一个数组来维护其特征信息。
也即使用我们此处的cnt数组来维护此字符串是否存在。继续看到上图,假设子节点b的idx
为3,那么对应我们在插入字符串后,会更新cnt[3]
的值为1,代表插入了一个字符串,对应为ab。
2.2 插入操作
在插入操作中,我们需要把一个一个的字符串,或者一个数串,插入到我们的查找树中。那么我们自然会需要遍历我们的树,按照什么顺序来遍历呢?
针对我们所需要插入的字符串,逐字符插入即可,我们令字符串为str[N]
。我直接给出对应代码:
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] ++;
}
- 代码中,我们初始化 p = 0,表示从根节点开始遍历。
- 接下来遍历遍历字符串 str的每个字符,将字符转换为数字 u。这个转换的方法很巧妙,总共26个字母,我们会把他们映射到0~25的索引上,而
str[i] - 'a'
就可以对应上其索引。这个减法本质上在用其ASCII表对应的数值进行计算,a-a的结果存为int就其值就变成0了,b-a的值即为1,…,以此类推。我们就完成了字符串中字母到数字的映射。 - 下一句我们看
if(!son[p][u]) son[p][u] = ++idx;
,其含义是如果son[p][u]
为 0,代表当前子节点没有这个字母,那么我们就分配一个新的节点并更新son[p][u]
。 - 最后让当前节点为子节点
son[p][u]
。 - 遍历完字符串后,更新
cnt[p]
,表示该节点截止的字符串多出现了一次。
2.3 查找操作
查找的思路和插入非常类似,可以直接看代码,如下:
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];
}
我们可以看到query代码其实和insert代码差不多,区别就是,我们没找到对应节点,即发现son[p][u]
的值为0时,直接返回0,代表没找到。
当能够完整遍历str字符串,即中间没有直接返回0时,代表此字符串存在,我们最后返回结果cnt[p]
。
3 参考例题
我们来完成一道例题巩固学习成果,此例题来自AcWing,是一道模板题。
题目信息:
完整代码:
#include <iostream>
using namespace std;
const int N = 100010;
int son[N][26],cnt[N],idx;// cnt : 存储cnt[idx]出现了几次,idx表示节点号
char a[N];
void insert(char str[])
{
int p = 0;
for(int i = 0;str[i];i++) // 因为c中字符串末尾为0.所以可以这样遍历
{
int u = str[i] - 'a'; // 将a~z映射至0~25
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p] ++;
}
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];
}
int main(){
int n ;
char op;
cin >> n;
while(n--)
{
cin >> op >> a;
if(op=='I') insert(a);
else cout << query(a) << endl;
}
return 0;
}
参考资料
1: AcWing 算法基础课