浅谈Trie树(字典树)

                                                                                            Trie树(字典树)php

1、引入node

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

字典树天然也是起查找做用的。查找的是啥?单词。数组

看如下几个题:数据结构

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

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

好。下一个ui

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

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

judge:n<=200000,TLE!

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

2、原理

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

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

由此能够看出:

一、字典树用边表示字母

二、有相同前缀的单词公用前缀节点,那咱们能够的得出每一个节点最多有26个子节点(在单词只包含小写字母的状况下)

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

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

3、基本操做

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

二、代码

void insert()//插入单词s
{
    len=strlen(s);//单词s的长度
    root=0;//根节点编号为0
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'a';//第二种编号
        if(!trie[root][id])//若是以前没有从root到id的前缀 
                    trie[root][id]=++tot;//插入,tot即为第一种编号
        root=trie[root][id];//顺着字典树往下走
    }
}

B、search,查找

查找有不少种,能够查找某一个前缀,也能够查找整个单词。

再次咱们以查找一个前缀是否出现过为例讲解

一、思路

  从左往右以此扫描每一个字母,顺着字典树往下找,能找到这个字母,往下走,不然结束查找,即没有这个前缀;前缀扫完了,表示有这个前缀。

二、代码

bool find()
{
    len=strlen(s);
    root=0;//从根结点开始找
    for(int i=0;s[i];i++)
    {
        int x=s[i]-'a';//
        if(trie[root][x]==0)   return false;//以root为头结点的x字母不存在,返回0 
        root=trie[root][x];//为查询下个字母作准备,往下走 
    }
    return true;//找到了
}

三、若是是查询某个单词的话,咱们用bool变量 v[i]表示节点i是不是单词结束的标志。

    那么最后return的是v[root],因此在插入操做中插入完每一个单词是,要对单词最后一个字母的v[i]置为true,其余的都是false

四、若是是查询前缀出现的次数的话,那就在开一个sum[],表示位置i被访问过的次数,

   那么最后return的是sum[root],插入操做中每访问一个节点,都要让他的sum++

   这里前缀的次数是标记在前缀的最后一个字母所在位置的后一个位置上。

  好比:前缀abc出现的次数标记在c所在位置的后一个位置上,

 

4、完整代码

一、查询是否出现

/*
  trie tree的储存方式:将字母储存在边上,边的节点链接与它相连的字母 
  trie[rt][x]=tot:rt是上个节点编号,x是字母,tot是下个节点编号 
*/ 
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#define maxn 2000010
using namespace std;
int tot=1,n;
int trie[maxn][26];
//bool isw[maxn];查询整个单词用
void insert(char *s,int rt)
{
    for(int i=0;s[i];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,int rt)
{
    for(int i=0;s[i];i++)
    {
        int x=s[i]-'a';
        if(trie[rt][x]==0)return false;//以rt为头结点的x字母不存在,返回0 
        rt=trie[rt][x];//为查询下个字母作准备 
    }
    return true;
    //查询整个单词时,应该return isw[rt] 
}
char s[22];
int main()
{
    tot=0;
    int rt=1;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        insert(s,rt);
    }
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        if(find(s,rt))printf("YES\n");
        else printf("NO\n");
    }
    return 0;
}
数组模拟

二、查询前缀出现次数

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int trie[400001][26],len,root,tot,sum[400001];
bool p;
int n,m; 
char s[11];
void insert()
{
    len=strlen(s);
    root=0;
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'a';
        if(!trie[root][id]) trie[root][id]=++tot;
        sum[trie[root][id]]++;//前缀保存 
        root=trie[root][id];
    }
}
int search()
{
    root=0;
    len=strlen(s);
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'a';
        if(!trie[root][id]) return 0;
        root=trie[root][id];
    }//root通过此循环后变成前缀最后一个字母所在位置
    return sum[root];
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        insert();
    }
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        cin>>s;
        printf("%d\n",search());
    }
}

数组模拟
数组模拟
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
char s[11];
int n,m;
bool p;
struct node
{
    int count;
    node * next[26];
}*root;
node * build()
{
    node * k=new(node);
    k->count=0;
    memset(k->next,0,sizeof(k->next));
    return k;
}
void insert()
{
    node * r=root;
    char * word=s;
     while(*word)
    {
        int id=*word-'a';
        if(r->next[id]==NULL) r->next[id]=build();
        r=r->next[id];
        r->count++;
        word++;
    }
}
int search()
{
    node * r=root;
    char * word=s;
    while(*word)
    {
        int id=*word-'a';
        r=r->next[id];
        if(r==NULL) return 0;
        word++;
    }
    return r->count;
}
int main()
{
    root=build();
    scanf("%d",&n);
    for(int i=1;i<=n;i++) 
    {
            cin>>s;
            insert();
    }
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        cin>>s;
        printf("%d\n",search());
    }
}
指针写法

5、模板题

hud 1251 统计难题 http://acm.hdu.edu.cn/showproblem.php?pid=1251

codevs 4189 字典 http://codevs.cn/problem/4189/