字符串处理——字典树

【概述】

字典树,又称为单词查找树,Tire 树,是一种树形结构,它是哈希树的变种。

字典树与字典很相似,当要查一个单词是不是在字典树中,首先看单词的第一个字母是不是在字典的第一层,如果不在,说明字典树里没有该单词,如果在就在该字母的孩子节点里找是不是有单词的第二个字母,没有说明没有该单词,有的话用同样的方法继续查找,以此类推。

基本性质:

  • 字典树用边表示字符
  • 有相同前缀的单词共用前缀结点
  • 根节点不包含字符
  • 每个单词结束的时候都用一个特殊字符表示,图中用的红色 $ 符
  • 从根节点到一个红点所经过的所有边的字母就是一个字符串

应用:其常用于统计、排序和保存大量的字符串(不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。

优点:其利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。 

【构建过程】

1.插入

1)思路

对于字典树,从左到右扫描一个单词,若字母在相应根节点下没有出现,就插入这个字母,若出现过,则沿着树走下去,看单词的下一个字母。

那么此时产生一个问题,对于从左到右扫描的这个单词,若字母在相应根节点下没有出现过,如何去选择位置插入?计算机不会自动选择位置,因此需要给他指定一个位置,这样就需要对每个字母进行编号。

设数组 tire[i][j]=k,表示编号为 i 的结点的第 j 个孩子是编号为 k 的结点,此时有两种编号:

  • i、k:表示结点位置的编号,相对整棵树而言,此处相同字母编号不同
  • j:表示几点 i 的第 j 个孩子,相对结点 i 而言,此处编码应按照 ASCLL 码来编,用到哪个编哪个,相同字母编号相同

以单词 cat,cash,app,apple,aply,ok 为例,按照输入顺序对其进行编号 

第一种编号结果:

第二种编号结果:

经过这样的编号后,这样数组 trie[i][j]=k,表示编号为 i 的节点的第 j 个孩子是编号为 k 的节点,那么第一种编号即为 i、k,第二种编号即为 j,从而可以实现插入

2)实现

int tot=0;//编号
int trie[N][26];//字典树
int val[N];//字符串结尾标记,val[i]=x表示第i个节点的权值为x
void insert(char *s){//插入单词s
    int len=strlen(s);//单词s的长度
    int root=0;//字典树上当前匹配到的结点
    for(int i=0;i<len;i++){
        int id=s[i]-'a';//子节点编号
        if(trie[root][id]==0)//如果之前没有从root到id的前缀 
            trie[root][id]=++tot;//插入
        root=trie[root][id];//顺着字典树往下走
    }
    val[root]=n;
}

2.查找

1)思路

查找有很多种,可以查找某一个前缀,也可以查找整个单词。一般来说,对于一个单词,从左到右依次扫描每个字母,顺着字典树往下找,能找到这个字母,往下走,否则结束查找,即没有这个单词;单词扫完了,表示有这个单词。

如果是查询某个单词是否在字典树中的话,可用布尔变量 vis[i] 表示节点 i 是否是单词结束的标志,那么返回的是 vis[root],所以在插入操作中插入完每个单词时,要对单词最后一个字母的 vis[i] 置为 true

如果是查询前缀出现的次数的话,那就在开一个数组 sum[i],表示位置 i 被访问过的次数,那么最后返回的是 sum[root],所以插入操作中每访问一个节点,都要让他的 sum++。在这里,前缀的次数是标记在前缀的最后一个字母所在位置的后一个位置上。

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

2)实现

bool find(char *s){//查询单词是否在树中
    int len=strlen(s);//单词长度
    int root=0;//从根结点开始找
    for(int i=0;s[i];i++){
        int x=s[i]-'a';
        if(trie[root][x]==0)//以root为头结点的x字母不存在
            return false;
        root=trie[root][x];//为查询下个字母做准备,往下走 
    }
    return val[root];//找到
}

3.删除

1)思路

对于一个单词,如果要在树中将其删除,有三种情况:

  • 没找到这个单词
  • 找到叶节点的时,叶节点的 cnt 标志清零,代表不是叶节点
  • 当前节点没有其他孩子节点的时,可直接删除这个节点

2)实现

void del(char *str,int word){//word为要删除的单词的标号,一般为seach("单词");
    int len=strlen(str);
    int root=0;
    if(word==0)//没找到单词
        return;
    for(int i=0;i<len;i++){
        int x=str[i]-'a';
        if(tire[root][x]==0)//没有子节点
            return;
        sum[root]-=cnt;//减去前缀
        root=tire[root][x];
    }
    sum[root]=0;//前缀清零
    for(int i=0;i<26;i++)//删除节点
        tire[root][i]=0;
}

【模版】

1.查询单词/前缀是否出现

int tot;
int trie[N][26];//trie[rt][x]=tot,root是上个节点编号,x是字母,tot是下个节点编号 
//bool vis[N];//查询整个单词用
void insert(char *s,int root){
    for(int i=0;s[i];i++){
        int x=s[i]-'a';
        if(trie[root][x]==0)//现在插入的字母在之前同一节点处未出现过 
            trie[root][x]=++tot;//字母插入一个新的位置,否则不做处理 
        root=trie[root][x];//为下个字母的插入做准备  
    }
    //vis[root]=true;//标志该单词末位字母的尾结点,在查询整个单词时用
}
bool find(char *s,int root){
    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;
    //return vis[root];//查询整个单词时
}

int main(){
    int n,m;
    char s[22];
    tot=1;

    cin>>n;//插入单词个数
    for(int i=1;i<=n;i++){
        cin>>s;
        insert(s,1);
    }

    cin>>m;//查询单词个数
    for(int i=1;i<=n;i++){
        cin>>s;
        if(find(s,1))
            printf("YES\n");
        else 
            printf("NO\n");
    }
    return 0;
}

2.查询前缀出现次数

int trie[400001][26],tot;
int sum[400001];
void insert(char *s){
    int root=0;
    int len=strlen(s);
    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(char *s){
    int root=0;
    int len=strlen(s);
    for(int i=0;i<len;i++){//root经过循环后变成前缀最后一个字母所在位置
        int id=s[i]-'a';
        if(!trie[root][id]) 
            return 0;
        root=trie[root][id];
    }
    return sum[root];
}
int main(){
    int n,m;
    char s[11];
    tot=1;

    cin>>n;//插入单词个数
    for(int i=1;i<=n;i++){
        cin>>s;
        insert(s);
    }

    cin>>m;//查询次数
    for(int i=1;i<=m;i++){
        cin>>s;
        printf("%d\n",search(s));
    }
}

3.结构体实现的增删查字典树

struct Node{
    int sum;//前缀
    int next[26];//子节点
    void init(){
        sum=0;
        memset(next,-1,sizeof next);
    }
}tire[N];
int tot;
void insert(char *str){
    int len=strlen(str);
    int root=0;
    for(int i=0;i<len;i++){
        int x=str[i]-'a';
        if(tire[root].next[x]==-1)
            tire[root].next[x]=tot++;
        root=tire[root].next[x];
        tire[root].sum++;
    }
}
int search(char *str){
    int len=strlen(str);
    int root=0;
    for(int i=0;i<len;i++){
        int x=str[i]-'a';
        if(tire[root].next[x]==-1)
            return 0;
        root=tire[root].next[x];
    }
    return tire[root].sum;
}
void del(char *str,int word){
    int len=strlen(str);
    int root=0;
    if(word<0)
        return;
    for(int i=0;i<len;i++){
        int x=str[i]-'a';
        if(tire[root].next[x]==-1)
            return;
        tire[root].sum-=word;
        root=tire[root].next[x];
    }
    tire[root].sum=0;

    for(int i=0;i<26;i++)
        tire[root].next[i]=-1;
}

int main(){
    tot=1;
    for(int i=0;i<N;i++)
        tire[i].init();

    int t;
    scanf("%d",&t);
    while(t--){
        char str[10],word[35];
        scanf("%s%s",str,word);
        if(str[0]=='i')//插入
            insert(word);
        else if(str[0]=='d')//删除
            del(word,search(word));
        else{//查询
            if(search(word)>0)
                printf("Yes\n");
            else
                printf("No\n");
        }
    }
    return 0;
}

4.输出唯一前缀

int tot;
int trie[N][26];//trie[rt][x]=tot,root是上个节点编号,x是字母,tot是下个节点编号
int val[N];
void insert(char *s,int root){
    int len=strlen(s);
    for(int i=0;i<len;i++){
        int x=s[i]-'a';
        if(trie[root][x]==0){//现在插入的字母在之前同一节点处未出现过
            trie[root][x]=tot;//字母插入一个新的位置,否则不做处理
            val[tot]=0;//记录以当前结点为根的子树下单词个数
            tot++;
        }
        root=trie[root][x];//为下个字母的插入做准备
        val[root]++;
    }
}
void find(char *s,int root){
    int len=strlen(s);
    for(int i=0;i<len;i++){
        int x=s[i]-'a';
        root=trie[root][x];//为查询下个字母做准备
        printf("%c",s[i]);//输出当前字母
        if(val[root]==1)//为1时直接返回
            return;
    }
}
char word[N][50];
int main(){
    int cnt=0;
    tot=1;
    while(scanf("%s",word[cnt])!=EOF)
        insert(word[cnt++],0);
    for(int i=0;i<cnt;i++){//枚举所有单词
        printf("%s ",word[i]);
        find(word[i],0);
        printf("\n");
    }
    return 0;
}

【例题】

  • 统计难题(HDU-1251)(查询前缀次数)点击这里
  • Hat’s Words(HDU-1247)(字典树+单词划分)点击这里
  • Problem C(HDU-5687)(带删除的字典树)点击这里
  • Phone List(HDU-1671)(有限空间+指针的灵活应用)点击这里
  • Shortest Prefixes(POJ-2001)(输出唯一前缀)点击这里
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值