Trie树【字典树】
一种高效地存储和查找字符串集合的数据结构
Trie 树是一种多叉树的结构,每个节点保存一个字符,一条路径表示一个字符串。
下图表示了字符串: him 、 her 、 cat 、 no 、 nova 构成的 Trie 树。
从图中可以看出 Trie 树包含以下性质:
-
根节点不包含字符,其他节点包含一个字符。
-
从根节点到某一节点经过的字符连接起来构成一个字符串。如图中的 him 、 her 、 cat 、 no 、 nova。
-
一个字符串与 Trie 树中的一条路径对应。
-
在实现过程中,会在叶节点中设置一个标志,用来表示该节点是否是一个字符串的结尾,本例中用青色填充进行标记。
如何生成 Trie 树?
Trie 树的生成过程,就是不断将字符串插入树中。
以插入字符串 him 、 her 、 cat 、 no 、 nova 为例,过程如下:
- 插入 him :
-
插入 her :
-
插入 cat:
-
插入 no:
-
插入 nova:
Trie 树有什么用?
Trie 树又叫字典树。字典是用来查字的,Trie 树最基本的作用是在树上查找字符串。
例如有 5 个字符串: him 、 her 、 cat 、 no 、 nova 。现在要查找 catch 是否存在。
如果使用暴力的方法,需要用 catch 与这 5 个字符串分别进行匹配,效率较低。
如果将这 5 个字符串存储成 Trie 的结构,只需要顺着路径依次比较,比较完 cat 之后,没有节点与 c 匹配,所以字符串集合中不存在 catch。
找:catch、he、no
Trie 树还有其他用途吗?
词频统计
在构造树的过程中,已经将所有字符串遍历了一遍。可以在 Trie 树节点的数据结构中,增加一个 count 来计数。对于每个字符串的插入操作,若已存在,计数加 1,若不存在,插入后 count 置为 1。
要统计某个字符串出现的次数,只需要找到字符串结尾对应的节点,输出对应节点的 count 值即可。
#include<iostream>
using namespace std;
const int N = 100010;
int idx; // 各个节点的编号,根节点编号为0 idx相当于一个分配器,如果需要加入新的结点就用++idx分配出一个下标
int son[N][26];//Trie 树本身 字符串最大长度N即trie最多N层,每个节点有26个状态(字符串仅包含26个小写英文字母)
//cnt[x] 表示:以 编号为 x 为结尾的字符串的个数
int cnt[N];
int n;
void insert(string s){
int p = 0;//指向根节点
for(int i = 0; i < s.size(); i++){
//将当前字符转换成数字(a->0, b->1,...)
int u = s[i] - 'a';
//如果数中不能走到当前字符
//为当前字符创建新的节点,保存该字符
if(!son[p][u])
// 新节点编号为 idx + 1
son[p][u] = ++idx;
p = son[p][u];
}
//这个时候,p 等于字符串 s 的尾字符所对应的 idx
//cnt[p] 保存的是字符串 s 出现的次数
//故 cnt[p] ++
cnt[p] ++;
}
int query(string s){
int p = 0;//指向根节点
for(int i = 0; i < s.size(); i++){
//将当前字符转换成数字(a->0, b->1,...)
int u = s[i] - 'a';
//如果走不通了,即树中没有保存当前字符
//则说明树中不存在该字符串
if(!son[p][u])
return 0;
//指向下一个节点
p = son[p][u];
}
//循环结束的时候,p 等于字符串 s 的尾字符所对应的 idx
// cnt[p] 就是字符串 s 出现的次数
return cnt[p];
}
int main(){
cin >> n;
string s;
char q;
while(n--){
cin >> q >> s;
if(q == 'I'){
//插入操作
insert(s);
}
else{
//查询操作
cout << query(s) << endl;
}
}
}
Trie 树的优缺点
Trie树的核心思想是空间换时间,利用字符串的公共前缀来减少无谓的字符串比较以达到提高查询效率的目的。
优点:插入和查询的效率很高,都为O(m)。其中 m 是待插入/查询的字符串的长度。
缺点:空间消耗比较大。