分析:
Trie 树是一种能够快速插入和查询字符串的多叉树结构。节点的编号各不相同,根节点编号为 0,其他节点用来标识路径,还可以标记单词插入的次数。
Trie 树最重要的是用 son 这个二维数组模拟多叉树的思想。其中 son[p][u] 表示从节点 p 沿着 u 这条边走到的子节点。边的选择有26(a-z 映射为 0-25) 个。也就是每个节点最多有 26 个分叉。
另外利用 cnt[p] 来存储以结点 p 结尾的单词的插入次数。
多次出现的 idx 用来给节点编号。(根据插入图进行正确理解)
插入
int son[N][26], cnt[N], idx;
char str[N];
void insert(char *str)
{
int p = 0; // p 指向根节点
for (int i = 0; str[i]; i ++ ) // 遍历字符串的每一个有效字符
{
int u = str[i] - 'a'; // u 为当前字符在(0-25)的映射
// 从上个节点沿 u 这条边找不到子节点,借助这条边创建子节点,更新 idx 并赋值
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u]; // p 指针移动到该子节点,继续插入后面的字符
}
cnt[p] ++ ; // 整个字符串插入结束,p 指向最后一个字符的节点,cnt[p] 存储末尾字符出现的次数
}
在空 Trie 中仅有一个根节点,编号为 0。从根开始插,枚举字符串的每个字符:① 如果有子节点(之前有边能通到),则 p 指针顺着之前出现的边走到该节点;② 如果没有子节点,则先创建子节点,根据当前字符映射的值创建边,p 指针再走到该子节点。在单词结束点记录插入次数。
下面模拟一下插入 “cat” 时树的形状及其参数变化:
- 遍历到 ‘c’ 时,c - ‘a’ 为 2,c 又是第 1 个插入的节点 idx 为 1,因此 son[0][2] = 1,也就是说第 1 个节点是根节点沿 2(c - ‘a’) 这条边走到的子节点。
- 遍历到 ‘a’ 时,a - ‘a’ 为 0,a 是第 2 个插入的节点 idx 为 2,因此 son[1][0] = 2,也就是说第 2 个节点是第 1 个节点沿 0(a - ‘a’) 这条边走到的子节点。
- 遍历到 ‘t’ 时,t - ‘a’ 为 19,t 是第 3 个插入的节点 idx 为 3,因此 son[2][19] = 3,也就是说第 3 个节点是第 2 个节点沿 19(t - ‘a’) 这条边走到的子节点。
下面画出以此插入 “cat” “car” “busy” “cate” “bus” “car”。用红色的星号来表示字符串结尾处。
这里需要注意的是 idx 的值的变化,只有当前插入的字符串字符是之前没有边到达过的,才会生成节点并更新 idx。这就方便了我们重复字符串的插入后的操作。
查询
int query(char *str)
{
int p = 0; // p 指向根节点
for (int i = 0; str[i]; i ++ ) // 遍历字符串的每一个有效字符
{
int u = str[i] - 'a'; // u 为当前字符在(0-25)的映射
if (!son[p][u]) return 0; // 从上个节点沿 u 这条边找不到子节点,该字符串没出现过
p = son[p][u]; // 出现过,p 指针移动到该子节点,继续判断后面的字符
}
return cnt[p]; // 返回以 p 为结尾的字符串的出现的次数
}
查询操作时,从根开始查扫描字符串,简单来说就是有字母 str[i],则走下来一直到词尾,返回插入次数;若无字母,返回0。
代码(C++)
#include <iostream>
using namespace std;
const int N = 100010;
int son[N][26], cnt[N], idx;
char str[N];
void insert(char *str)
{
int p = 0; // p 指向根节点
for (int i = 0; str[i]; i ++ ) // 遍历字符串的每一个有效字符
{
int u = str[i] - 'a'; // u 为当前字符在(0-25)的映射
// 从上个节点沿 u 这条边找不到子节点,借助这条边创建子节点,更新 idx 并赋值
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u]; // p 指针移动到该子节点,继续插入后面的字符
}
cnt[p] ++ ; // 整个字符串插入结束,p 指向最后一个字符的节点,cnt[p] 存储末尾字符出现的次数
}
int query(char *str)
{
int p = 0; // p 指向根节点
for (int i = 0; str[i]; i ++ ) // 遍历字符串的每一个有效字符
{
int u = str[i] - 'a'; // u 为当前字符在(0-25)的映射
if (!son[p][u]) return 0; // 从上个节点沿 u 这条边找不到子节点,该字符串没出现过
p = son[p][u]; // 出现过,p 指针移动到该子节点,继续判断后面的字符
}
return cnt[p]; // 返回以 p 为结尾的字符串的出现的次数
}
int main()
{
int n;
cin >> n;
while (n --)
{
char op;
cin >> op >> str;
if (op == 'I') insert(str);
else cout << query(str) << endl;
}
return 0;
}