Trie树介绍
Trie树,也叫字典树,前缀树。是一种用于实现字符串快速检索
的多叉树
数据结构。它支持两种操作:
在树中插入一个字符串
字符串检索
Trie树的每个节点都拥有若干个字符指针,它可以用来插入字符串或者进行字符串检索。
Trie树的基本操作
初始化
Trie树刚开始时只有一个根节点,它的所有指针均指向NULL
。
根节点并不存储信息,只作为树的标识,进行字符串插入和检索时从根节点进入并扫描其节点,原因也容易理解,根节点只有一个,所以只能存储一个字符,如果要在根节点存入字符的话,就需要开多棵Trie树分别存入不同的开头字母,这显然有些多此一举了。
插入
假设我们此时需要插入一个字符串s
,起初我们令一个指针p
指向根节点,依次扫描s
的每个字符c
,此时我们会发现有两种情况:
p
的c
字符指针指向了一个已存在的节点q
,则p
直接走到q
,然后更新p
,即p = q
p
的c
指针指向空,此时我们新建一个节点q
,然后将p
走到q
,然后更新p
,即p = q
当字符串s
扫描完毕时,在当前节点打上标记。因为如果不打上标记的话,假如此时存入了一个字符串
b
a
t
m
a
n
batman
batman,然后我们要查询是否有存入过
b
a
t
bat
bat,没有标记的话,就无法判断是否插入过
b
a
t
bat
bat了。
检索
检索和插入很像。
假设我们此时需要检索一个字符串s
,起初我们令一个指针p
指向根节点,依次扫描s
的每个字符c
,此时我们会发现有两种情况:
p
的c
字符指针指向了一个已存在的节点q
,则p
直接走到q
,然后更新p
,即p = q
p
的c
指针指向空,则代表该字符串没有插入过,此时我们结束检索
当s
中的字符扫描完毕时,若当前p有被标记过,则说明s
在Trie中存在,否则不存在。
示例
假设此时我们需要插入一个字符串cat
-
创建指针
p
进入根节点,扫描其子节点,发现并没有c
这个节点,因此创建一个c
节点,然后将p
移动到c
-
p
扫描c
的子节点,发现并没有a
字符,创建并移动到a
-
p
扫描a
的子节点,发现并没有t
,创建并移动到t
-
此时我们发现字符串已经扫描完毕,
标记p
假设我们此时还需要插入一个字符串cab
-
创建指针
p
进入根节点,扫描其子节点有没有字符c
,发现是有的,直接进入c
-
扫描
p
的子节点有没有a
,发现是有的,直接进入a
-
扫描
p
的子节点,看看有没有b
,发现并没有,创建并将p
移动到节点b
-
此时发现字符串
cab
已扫描完毕,标记p
其余也是类似,此处就不再过多说明。
例题
问题描述
维护一个字符串集合,支持两种操作:
I x
:向集合中插入一个字符串 x
;
Q x
:询问一个字符串在集合中出现了多少次。
共有N
个操作,输入的字符串总长度不超过
1
0
5
10^5
105,字符串仅包含小写英文字母。
输入格式
第一行包含整数N
,表示操作数。
接下来N
行,每行包含一个操作指令,指令为I x
或Q x
中的一种。
输出格式
对于每个询问指令Q x
,都要输出一个整数作为结果,表示x
在集合中出现的次数。
每个结果占一行。
数据范围
1 ≤ N ≤ 2 ∗ 1 0 4 1≤N≤2∗10^4 1≤N≤2∗104
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
解题思路
思路1:unordered_map(哈希表)
此题只有插入和查询字符串次数两个操作,那么用哈希表显然是一种快准狠的方法,在此就不多赘述了,读者感兴趣可以自己写写
思路1代码
#include<iostream>
#include<unordered_map>
#include<cstring>
using namespace std;
int n;
unordered_map<string,int> mp;
int main(){
cin >> n;
while(n --){
char op[2];
string str;
cin >> op >> str;
if(op[0] == 'I') mp[str] ++;
else printf("%d\n",mp[str]);
}
return 0;
}
思路2:
使用Trie树,此时我们会发现询问操作是询问我们字符串出现次数,那么插入结束时的标记操作就可以转换为计数操作(插入结束时在尾部 + 1),其余并没有什么变化。
代码
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100010;
int n;
char str[N];
int son[N][26],cnt[N],idx;
void insert(char str[]){ // 插入字符串str
int p = 0; // 创建p指向根节点
for(int i = 0; str[i]; i ++){ // 遍历待插入字符串
int u = str[i] - 'a'; // 将 a ~ z 映射为 0 ~ 25
if(!son[p][u]) son[p][u] = ++ idx; // 如果p没有u这个子节点,则创建出该子节点
p = son[p][u]; // 将p移动到该子节点中
}
cnt[p] ++; // 表示该字符串出现次数 + 1
}
int query(char str[]){ // 检索
int p = 0; // 创建指针p指向根节点
for(int i = 0; str[i]; i ++){ // 遍历待检索字符串
int u = str[i] - 'a'; // 将 a ~ z 映射为 0 ~ 25
if(!son[p][u]) return 0; // 如果p没有u这个子节点,说明没有该字符串,返回0
p = son[p][u]; // 将p移动到该子节点中
}
return cnt[p]; // 返回字符串出现的次数
}
int main(){
scanf("%d",&n);
while(n --){
char op[2];
scanf("%s%s",op,str);
if(op[0] == 'I') insert(str); // 插入
else printf("%d\n",query(str)); // 检索
}
return 0;
}