Trie树初步学习

什么是Trie树

Trie树有很多名字,字典树,前缀树。

Trie树主要用来高效存储字符串或者二进制数,可以避免重复元素的存储,同时提高查找效率。

我们以存储26个小写字母为例
由于有26个字母,所以从根节结点往下开始,每个父结点都最多有26个孩子结点。

1. 插入操作

Trie树的创建是从根节点开始,假设我们要插入字符串”in”。

  1. 我们一开始位于根,也就是0号节点,我们用P=0表示。我们先看P是不是有一条标识着 字符i 的连向子节点的边。发现没有这条边,于是我们就新建一个节点,也就是1号节点,并且将边标识为 字符i。然后我们移动到1号节点,也就是令P=1。这样我们就把 ”in ”的 i字符 插入到Trie树中了。

  2. 然后我们再插入字符n,也是先找P,也就是1号节点有没有标记为 字符n 的边,还是没有,于是再新建一个节点2,并且把边标识为 字符n。最后再移动到P=2。这样我们就把n也插入了。

  3. 由于n是”in”的最后一个字符,所以我们还需要将P=2这个节点标记为终结点

其它字符串的插入操作同理,如果已有该孩子结点,就不用再创建了,趁着即可,如果没有的话,再创建。

2. 查找操作

如何查询Trie树中是不是包含字符串S?

我们只要从根节点开始,沿着标识着S[0]->S[1] -> S[2] -> S[3] … -> S[S.len]的边移动,

  1. 如果最后成功到达一个终结点,就说明S在Trie树中;
  2. 如果最后无路可走,或者到达一个不是终结点的节点,就说明S不在Trie树中。

3. 二维数组模拟Trie树

3.1 实现存储功能

int trie[M][26];
仍以存储26个小写字母的trie树为例,
M的值取决所有输入的字符串中字符的个数总和,最坏情况下可能给每个字符都建立一个结点;
26是因为最多有26个分支。

用一个二维数组来模拟一个Trie树:

一、trie[i] 代表二维数组的第 i 行,每一行的编号 i 即对应上图中Trie树中结点编号p的含义:i==p

注意有可能根节点的26个子节点的结点编号不是1到26,这与这个字符出现的先后顺序有关
即有可能trie[0][3]=233; 即根节点的第3个分支,结点编号为233,在二维数组的第233行trie[233]

这需要再额外定义一个变量 int idx=1;维护,0号结点是根节点,板上钉钉,每次从根节点开始搜索、插入,所以idx直接从1开始。 存不同的字符,先出现哪个字符,就先伸出哪条边进行存储。
idx的作用等同于单链表中idx的作用。

二、每一行都有26列,代表有26个分支,这里只是从a-z,即从0到25;

charset[i]=ch; i从0到25,分别对应小写字母a到z,即charset[i]=i+'a';

三、trie[i][j]=x;的含义:

  1. 若x等于0,则说明结点编号为i的结点没有伸出chaset[j]的边,即没有到chaset[j]的子节点;
  2. 若x不等于0,则说明结点编号为i的结点有伸出chaset[j]的边,即有到chaset[j]的子节点,结点编号下标为x必有x>i

那么具体是怎么存储字符的呢?

trie[0]就是根节点,结点编号i是0,整个二维数组存储的值含义都是它的下一个结点编号是多少,
trir[i][j]=x 这样一个存值的过程,表示从结点i可以到结点x,伸出了一条charset[j]的边,即存储字符charset[j],所以是通过结点之间的边存储的,不是通过结点本身。

3.2 实现标记结束功能

以上面的图中的t-e-a路径为例,图中只有a结点标记了结束标记,所有只有字符串"tea",而没有字符串"te";
再看路径i-n-t, n结点和t结点都有结束标记,所以trie树中存有字符串"in"和"int"。

int cnt[M];
我们再开一个cnt数组来维护Trie树中每个结点编号,即二维数组中的每一行。
cnt[i]=x;的含义是:以结点编号为i结尾的字符串的个数为x个,然而只有唯一的一条路径能指向结点编号为i的结点,所以即统计某个字符串出现的次数。

题目描述

维护一个字符串集合,支持两种操作:

“I x”向集合中插入一个字符串x;
“Q x”询问一个字符串在集合中出现了多少次。
共有N个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。

输入格式
第一行包含整数N,表示操作数。

接下来N行,每行包含一个操作指令,指令为”I x”或”Q x”中的一种。

输出格式
对于每个询问指令”Q x”,都要输出一个整数作为结果,表示x在集合中出现的次数。

每个结果占一行。

数据范围
1≤N≤2∗104
输入样例:

5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:

1
0
1

算法实现

#include <iostream>
#define read(x) scanf("%d",&x)

using namespace std;

const int M=1e5+10;
int trie[M][26],cnt[M],idx=1;  "0号结点是根节点,有效的字符存储是从下标1开始的,i->j的边才表示一个字符:0->1"
                                                   "0->1的边每次存储哪个字符是不确定的,i->j同理"
void insert(char *str)
{
    
    
    int p=0;  //从根节点开始插入,p表示节点编号
    for (int i=0;str[i]!='\0';i++) {
    
    
        int t=str[i]-'a'; //指向具体哪个分支
        if (trie[p][t]==0) trie[p][t]=idx++; //如果分支不存在的话,开辟一个新结点
        p=trie[p][t];   //指向该分支存储在的下一个结点,形成了边,到这一步时才说明插入字符str[i]了
    }
    cnt[p]++; //统计字符串个数
}

int find(char *str)
{
    
    
    int p=0; //从根节点开始查找,p表示节点编号
    for (int i=0;str[i]!='\0';i++) {
    
    
        int t=str[i]-'a';
        if (!trie[p][t]) return 0; //顶点p->triep[p][t]不可达,说明不存在边str[i]了,即没有存储字符str[i]
        p=trie[p][t];
    }
    return cnt[p];
}

int main()
{
    
    
    int n;
    read(n);
    char op[3],str[M];
    while (n--) {
    
    
        scanf("%s%s",op+1,str);   "尝试从下标1开始输入字符串,注意'\0'也要占位置"
        if(op[1]=='I') insert(str);
        else printf("%d\n",find(str));
    }
    
    return 0;
}

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/114178852