字典树系统学习

学习博客:https://blog.csdn.net/SongBai1997/article/details/82317259

这篇博客讲的挺好的,很详细,很好理解。

Trie树(字典树)

一、引入

字典是干啥的?查找字的。

字典树自然也是起查找作用的。查找的是啥?单词。

看以下几个题:

1、给出n个单词和m个询问,每次询问一个单词,回答这个单词是否在单词表中出现过。

答:简单!map,短小精悍。

好。下一个

2、给出n个单词和m个询问,每次询问一个前缀,回答询问是多少个单词的前缀。

答:map,把每个单词拆开。

judge:n<=200000,TLE!

这就需要一种高级数据结构——Trie树(字典树)

二、原理

在本篇文章中,假设所有单词都只由小写字母构成

对cat,cash,app,apple,aply,ok 建一颗字典树,建成之后如下图所示

字典树系统学习

由此可以看出:

1、字典树用边表示字母

2、有相同前缀的单词公用前缀节点,那我们可以的得出每个节点最多有26个子节点(在单词只包含小写字母的情况下)

3、整棵树的根节点是空的。为什么呢?便于插入和查找,这将会在后面解释。

4、每个单词结束的时候用一个特殊字符表示,图中用的‘′,那么从根节点到任意一个‘′,那么从根节点到任意一个‘’所经过的边的所有字母表示一个单词。

三、基本操作

A、insert,插入一个单词

1.思路

  从图中可以直观看出,从左到右扫这个单词,如果字母在相应根节点下没有出现过,就插入这个字母;否则沿着字典树往下走,看单词的下一个字母。

  这就产生一个问题:往哪儿插?计算机不会自己选择位置插,我们需要给它指定一个位置,那就需要给每个字母编号。

  我们设数组trie[i][j]=k,表示编号为i的节点的第j个孩子是编号为k的节点。

 什么意思呢?

 这里有2种编号,一种是i,k表示节点的位置编号,这是相对整棵树而言的;另一种是j,表示节点i的第j的孩子,这是相对节点i而言的。

 不理解?看图

 还是单词cat,cash,app,apple,aply,ok 

 我们就按输入顺序对其编第一种号,红色表示编号结果。因为先输入的cat,所以c,a,t分别是1,2,3,然后输入的是cash,因为c,a是公共前缀,所以从s开始编,s是4,以此类推。

注意这里相同字母的编号可能不同

 字典树系统学习

 第二种编号,相对节点的编号,紫色表示编号结果。

因为每个节点最多有26个子节点,我们可以按他们的字典序从0——25编号,也就是他们的ASCLL码-a的ASCLL码。

注意这里相同字母的编号相同

字典树系统学习

 实际上每个节点的子节点都应该从0编到——25,但这样会发现许多事根本用不到的。比如上图的根节点应该分出26个叉。节约空间,用到哪个分哪个。

 这样编号有什么用呢?

回到数组trie[i][j]=k。 数组trie[i][j]=k,表示编号为i的节点的第j个孩子是编号为k的节点。

那么第二种编号即为j,第一种编号即为i,k

2、代码

void Insert(char s[maxn],int rt)
{
    int len=strlen(s);
    for(int i=0;i<len;i++)
    {
        int x=s[i]-'a';
        if(trie[rt][x]==0)//现在插入的字母在之前同一节点未出现过
        {
            trie[rt][x]=++tot;//字母插入一个新的位置 如出现过则不用再存了
        }
        rt=trie[rt][x];//为下一个字母插入做准备
    }
    isw[rt]=true;//标记这个是单词
}

B、search,查找

查找有很多种,可以查找某一个前缀,也可以查找整个单词。

再次我们以查找一个单词是否出现过为例讲解

1、思路

  从左往右以此扫描每个字母,顺着字典树往下找,能找到这个字母,往下走,否则结束查找,即没有这个单词;扫完了,判断是否有这个单词就行了。

2、代码

bool Find(char s[maxn],int rt)
{
    int len=strlen(s);
    for(int i=0;i<len;i++)
    {
        int x=s[i]-'a';
        if(trie[rt][x]==0) return false;
        rt=trie[rt][x];//为查询下一个字母做准备
    }
    return isw[rt];//为啥需要这个呢?  因为你到了这里未必就有这个单词 可能只是其他单词的一个前缀罢了
}

完整代码:单词是否出现过:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=1e6+5;
int trie[maxn][30];
bool isw[maxn];
int tot=0;
void Insert(char s[maxn],int rt)
{
    int len=strlen(s);
    for(int i=0;i<len;i++)
    {
        int x=s[i]-'a';
        if(trie[rt][x]==0)//现在插入的字母在之前同一节点未出现过
        {
            trie[rt][x]=++tot;//字母插入一个新的位置 如出现过则不用再存了
        }
        rt=trie[rt][x];//为下一个字母插入做准备
    }
    isw[rt]=true;//标记这个是单词
}
bool Find(char s[maxn],int rt)
{
    int len=strlen(s);
    for(int i=0;i<len;i++)
    {
        int x=s[i]-'a';
        if(trie[rt][x]==0) return false;
        rt=trie[rt][x];//为查询下一个字母做准备
    }
    return isw[rt];//为啥需要这个呢?  因为你到了这里未必就有这个单词 可能只是其他单词的一个前缀罢了
}
int main()
{

    int rt=0;
    int N;//有N个单词
    scanf("%d",&N);
    char s[maxn];
    for(int i=1;i<=N;i++)
    {
        scanf("%s",s);
        Insert(s,rt);
    }
    scanf("%d",&N);//查询N个单词是否出现过
    for(int i=1;i<=N;i++)
    {
        scanf("%s",s);
        if(Find(s,rt)) printf("YES
");
        else printf("NO
");
    }
    return 0;
}