【数据结构】字典树/Trie树/前缀树 - 字符串的统计、排序和保存

字典树

描述

字典树,又称单词查找树、Trie树、前缀树,是一种树形结构,是一种哈希树的变种。

典型应用是用于统计、排序和保存大量的字符串(但不仅限于字符串)。

常见操作有插入和查找,删除操作少见。

性质

  • 根节点不包含字符
  • 除根节点外每一个节点都只包含一个字符
  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  • 每个节点的所有子节点包含的字符都不相同

优点

  • 利用字符串的公共前缀来减少查询时间
  • 最大限度地减少无谓的字符串比较
  • 查询效率比哈希树高
  • 自带字典序排序
  • 直接判断重复,或者记录相同单词重复的次数

缺点

  • 内存消耗非常大

储存结构图示

如图,最上方的节点为根节点,根节点不包含字符

从根节点开始到每一个红点的路径上字符拼接而成的字符串就是一个单词

该图中拥有的单词
abc
abcd
abd
b
bcd
efg
hik

储存方式与代码实现

常见的Trie树储存方式共有两种

代码以下述功能为例:

  • 输入n和m
  • 输入n个单词建树
  • 输入m个字符串s,要求输出以s为前缀的单词总数

储存方式 一

  直接用数组来储存下一个节点的位置

const int MAXN=1e5+50;
int tree[MAXN][26]; //tree[i][j] 表示节点i的第j种儿子的节点id
int flag[MAXN]; //flag[i] 表示节点i建树时访问过的次数(前缀数量)
int cnt=0; //cnt储存目前最大的节点编号,添加节点时调用

  这种写法写起来最简单,但是空间消耗巨大(256MB理想状态最多也只能开250w)

  不论节点 i 的第 j 种儿子是否存在,都需要给他开一个int的空间来储存

  规定id=0为根节点,tree值为0表示无儿子节点

添加单词 addNode

void addNode(char *str)
{
    int root=0,id; //root表示处理到某个字符时上一个节点的id
    for(int i=0;str[i]!=‘\0‘;i++)
    {
        id=str[i]-‘a‘;
        if(tree[root][id]==0)
            tree[root][id]=++cnt;
        root=tree[root][id];
        flag[root]++;
    }
}

  输入单词,从头开始循环单词,root用来迭代节点的id,初始值为0

  如果root节点的第id个孩子不存在(说明之前没有访问过)

  让他的值变为++cnt来动态把节点加在数组可行部分的后面

  因为要求的是以某个字符串为前缀的单词数,所以把树上经过某个节点的次数给记录下来,让 flag+1

查询单词数 query

int query(char *str)
{
    int root=0,id;
    for(int i=0;str[i]!=‘\0‘;i++)
    {
        id=str[i]-‘a‘;
        if(tree[root][id]==0)
            return 0; //查询的时候出现节点不存在的情况的话,说明不存在这样前缀的单词
        root=tree[root][id];
    }
    return flag[root];
}

  相似于添加单词的代码,只不过不需要对树的结构进行改变

  如果在查找的过程中遇到路径上某个节点的儿子不存在,说明不存在这样前缀的单词(所以建树时没有加入这个节点)

  退出循环时root指向的节点正好为字符串最后一个字符表示的节点,将访问次数输出即可

储存方式 二

  采用结构体指针的方式来储存信息

struct node
{
    int flag;
    node *next[26];
};
node root;

  这种写法采用的是动态分配空间的方法,只有在使用时才分配空间,相对于上一种写法能节省更多的空间

  但是如果出现多组数据的情况,上一种写法只能采用memset循环使用空间,而这种写法如果要释放内存的话还需要写一个内存释放函数

  如果不释放内存,则只需要重置根节点的26个指针为NULL即可

添加单词 addNode

const int MAXLEN=100;
char str[MAXLEN];

void addNode() //循环法
{
    node *nd=&root;
    node *tmp;
    int id;
    for(int i=0;str[i]!=‘\0‘;i++)
    {
        id=str[i]-‘a‘;
        if(nd->next[id]==NULL)
        {
            tmp=(node*)malloc(sizeof(node));
            for(int j=0;j<26;j++)
                tmp->next[j]=NULL;
            tmp->flag=1;
            nd->next[id]=tmp;
            nd=tmp; //指针迭代
        }
        else
        {
            nd=tmp;
            nd->flag++;
        }
    }
}

void addNode(node *nd,int pos) //迭代法,调用addNode(&root,0)
{
    if(str[pos]==‘\0‘)
        return;
    int id=str[pos]-‘a‘;
    if(nd->next[id]==NULL) //将 nd->next[id] 看成这一部分的整体
    {
        nd->next[id]=(node*)malloc(sizeof(node));
        for(int i=0;i<26;i++)
            nd->next[id]->next[i]=NULL;
        nd->next[id]->flag=1;
        addNode(nd->next[id],pos+1);
    }
    else
    {
        nd->next[id]->flag++;
        addNode(nd->next[id],pos+1);
    }
}

  在节点不存在时动态申请空间

查询单词数 query

const int MAXLEN=100;
char str[MAXLEN];

int query() //循环法
{
    node *nd=&root;
    for(int i=0;str[i]!=‘\0‘;i++)
    {
        if(nd==NULL)
            return 0; //执行过程中找到不存在的节点,说明前缀不存在
        nd=nd->next[str[i]-‘a‘];
    }
    return nd->flag;
}

int query(node* nd,int pos) //迭代法,调用query(&root,0)
{
    if(nd==NULL)
        return 0; //执行过程中找到不存在的节点,说明前缀不存在
    if(str[pos]==‘\0‘)
        return nd->flag; //找到最后一个位置时直接返回flag值
    return query(nd->next[str[pos]-‘a‘],pos+1);//否则返回下一次迭代的值
}

释放空间 del

void del(node *nd) //迭代释放空间
{
    for(int i=0;i<10;i++)
        if(nd->next[i]!=NULL)
            del(nd->next[i]);//先释放深度更大的节点
    delete nd;//再释放自己
}

/*
注意,释放的对象原则上是只能针对动态申请空间的对象
所以不能直接调用&root
而是对root的子节点判断后释放
所以多组样例的情况初始化空间应该如下处理
*/
for(int i=0;i<10;i++)
{
    if(root.next[i]!=NULL)
        del(root.next[i]);
    root.next[i]=NULL;
}

  指针储存方式优点有很多,但是要求对指针操作熟练

  注意不要出现空指针操作!

?

?

字典树基本题型的代码模板

均采用上述储存方式 二 的写法

题型一:询问指定前缀的单词个数

思路同上文,记录路径经过的次数即可

HDU 1251 统计难题

#include<bits/stdc++.h>
using namespace std;
struct node
{
    int flag;
    node *next[26];
};

node root;
char str[233];

void addNode(node *nd,int pos)
{
    if(str[pos]==‘\0‘)
        return;
    int id=str[pos]-‘a‘;
    if(nd->next[id]==NULL) //将 nd->next[id] 看成这一部分的整体
    {
        nd->next[id]=(node*)malloc(sizeof(node));
        for(int i=0;i<26;i++)
            nd->next[id]->next[i]=NULL;
        nd->next[id]->flag=1;
        addNode(nd->next[id],pos+1);
    }
    else
    {
        nd->next[id]->flag++;
        addNode(nd->next[id],pos+1);
    }
}
int query(node* nd,int pos)
{
    if(nd==NULL)
        return 0; //执行过程中找到不存在的节点,说明前缀不存在
    if(str[pos]==‘\0‘)
        return nd->flag; //找到最后一个位置时直接返回flag值
    return query(nd->next[str[pos]-‘a‘],pos+1);//否则返回下一次迭代的值
}
int main()
{
    int i;
    for(i=0;i<26;i++)
        root.next[i]=NULL;
    while(true)
    {
        i=0;
        while((str[i]=getchar())!=‘\n‘)
            i++;
        if(i==0)
            break;
        str[i]=‘\0‘;
        addNode(&root,0);
    }
    while(scanf("%s",str)!=EOF)
        printf("%d\n",query(&root,0));

    return 0;
}

题型二:询问不同单词的数量

flag设为bool类型变量

为true表示有单词以当前节点作为结尾

在每次输入后先判断是否已经存在单词,如果不存在再进行插入操作并让答案+1即可

HDU 2072 单词数

贼恶心的题目,正常读入注意 空行/行首有空格/行末有空格/单词之间间隔2个及以上空格 的情况,或者读入行后使用stringstream处理

#include<bits/stdc++.h>
using namespace std;
struct node
{
    bool flag;
    node *next[26];
};

node root;
char str[233];
int ans;

void addNode(node *nd,int pos)
{
    if(str[pos]==‘\0‘)
    {
        if(nd->flag) //如果已经是某个单词的结尾,答案-1
            ans--;
        nd->flag=true; //标记结尾
        return;
    }
    int id=str[pos]-‘a‘;
    if(nd->next[id]==NULL) //将 nd->next[id] 看成这一部分的整体
    {
        nd->next[id]=(node*)malloc(sizeof(node));
        nd->next[id]->flag=false; // 初始置于false
        for(int i=0;i<26;i++)
            nd->next[id]->next[i]=NULL;
        addNode(nd->next[id],pos+1);
    }
    else
        addNode(nd->next[id],pos+1);
}

string s;
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    while(getline(cin,s))
    {
        if(s[0]==‘#‘)
            break;
        int i,j,len;
        ans=0;
        for(i=0;i<26;i++)
            root.next[i]=NULL;
        root.flag=false;
        len=s.length();
        s=s+" ";//最后面加个空格方便处理
        i=0;
        while(s[i]==‘ ‘)//处理行首空格
            i++;
        while(i<len)
        {
            j=0;
            while(s[i]!=‘ ‘)
                str[j++]=s[i++];
            while(s[i]==‘ ‘)//处理单词间空格
                i++;
            str[j]=‘\0‘;
            addNode(&root,0);
            ans++;
        }
        cout<<ans<<‘\n‘;
    }

    return 0;
}
/*
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    while(getline(cin,s))
    {
        if(s[0]==‘#‘)
            break;
        int i,j;
        ans=0;
        for(i=0;i<26;i++)
            root.next[i]=NULL;
        root.flag=false;
        stringstream in(s);
        while(in>>str)
        {
            buildTree(&root,0);
            ans++;
        }
        cout<<ans<<‘\n‘;
    }

    return 0;
}
*/

题型三:多个字符串判断是否存在一个字符串是另一个的前缀

挺常见的一种题型,因为数据范围大所以只能通过字典树判断

HDU 1671 Phone List Memory Limit 32MB

POJ 3630 Phone List Memory Limit 64MB

本题对空间有要求,不动态释放内存的话POJ不知道能不能过,但是HDU会MLE

想法就是在插入单词的过程中要保证:

  • 路径上的节点不能存在其他单词的结束标记(也就是插入单词的前缀)
  • 路径结束时准备标记的节点不能拥有子节点(插入单词是别的单词的前缀)

然后在每个样例前释放一次内存即可

#include<bits/stdc++.h>
using namespace std;
struct node
{
    bool flag;
    node *next[10];
}root;

char str[20];

bool addNode()
{
    int i,j,id;
    node *nd=&root;
    for(i=0;str[i]!=‘\0‘;i++)
    {
        id=str[i]-‘0‘;
        if(nd->next[id]==NULL)
        {
            nd->next[id]=(node*)malloc(sizeof(node));
            nd->next[id]->flag=false;
            for(j=0;j<10;j++)
                nd->next[id]->next[j]=NULL;
            nd=nd->next[id];
        }
        else
        {
            if(nd->next[id]->flag)
                return false;
            nd=nd->next[id];
        }
    }
    for(i=0;i<10;i++)
        if(nd->next[i]!=NULL)
            return false;
    nd->flag=true;
    return true;
}

void del(node *nd)
{
    for(int i=0;i<10;i++)
        if(nd->next[i]!=NULL)
            del(nd->next[i]);
    delete nd;
}

void solve()
{
    int n,i;
    bool flag=true;//标记答案是否合法
    for(int i=0;i<10;i++)
    {
        if(root.next[i]!=NULL)
            del(root.next[i]);
        root.next[i]=NULL;
    }
    root.flag=false;
    scanf("%d",&n);
    while(n--)
    {
        scanf("%s",str);
        if(flag&&!addNode())//如果答案已经不合法,不需要继续标记(依靠&&运算符的运算规则,左false则右不执行)
            flag=false;
    }
    puts(flag?"YES":"NO");
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
        solve();

    return 0;
}

题型四:多个字符串求互不重复的最短前缀(唯一前缀标识)

因为要考虑到所有情况互不重复,所以要先建完树再去处理而不是边建树边处理(否则后面的答案可能不是最优)

POJ 2001 Shortest Prefixes

此时的flag记录的是节点走过的次数

如果flag值为1,说明接下来这条路径只包含一个单词,说明就是个特征单词

此时只需要输出到第一次遇见flag为1的节点即可

#include<cstdio>
#include<cstdlib>
using namespace std;
struct node
{
    int flag;
    node *next[26];
}root;

char str[1050][30];

void addNode(int pos)
{
    node *nd=&root;
    for(int i=0;str[pos][i]!=‘\0‘;i++)
    {
        int id=str[pos][i]-‘a‘;
        if(nd->next[id]==NULL)
        {
            nd->next[id]=(node*)malloc(sizeof(node));
            nd=nd->next[id];
            nd->flag=1;
            for(int j=0;j<26;j++)
                nd->next[j]=NULL;
        }
        else
        {
            nd=nd->next[id];
            nd->flag++;
        }
    }
}

char tmp[30];
char* query(int pos)
{
    int i,id;
    node *nd=&root;
    for(i=0;str[pos][i]!=‘\0‘;i++)
    {
        tmp[i]=str[pos][i];
        id=str[pos][i]-‘a‘;
        if(nd->next[id]->flag==1)//如果接下来的路只包含一个单词,则把这个第一个节点记录下来就可以作为特征答案
        {
            tmp[i+1]=‘\0‘;
            return tmp;
        }
        nd=nd->next[id];
    }
    tmp[i]=‘\0‘;//到最后都没有遇到路径为1的情况,说明这个单词是别的单词的前缀,所以输出整个单词
    return tmp;
}

int main()
{
    int i,cnt=0;
    root.flag=0;
    for(i=0;i<26;i++)
        root.next[i]=NULL;
    while(scanf("%s",str[cnt])!=EOF)
        addNode(cnt++);
    for(i=0;i<cnt;i++)
        printf("%s %s\n",str[i],query(i));

    return 0;
}

持续补题……

原文地址:https://www.cnblogs.com/stelayuri/p/12562945.html

时间: 2024-12-12 01:16:38

【数据结构】字典树/Trie树/前缀树 - 字符串的统计、排序和保存的相关文章

Trie(前缀树)

1 什么是Trie Trie也叫前缀树,因为存放和查找的时候都是将关键字字符串从前到后一个字母一个字母的进行的,所以叫前缀树.根节点不存放字母,其它的每个结点都存放一个字母.如果某个结点的字母是要给关键字的最后一个字母,那么该节点还存放该路径对应的关键字的值.也就是说,整个关键字字符串存放在一条路径上.另外前缀树是有序树,每个结点的左子树存放的字母比该结点的要小,右子树存放的字母比该结点要大. 2 怎样创建一棵前缀树 可以用三个数组实现一棵前缀树,一个数组是树本身,一个是key,一个是value

·专题」 Trie(前缀树)

重新整理Trie的内容,还有一种叫做双链键树,不过到现在也不会写. Trie 可以称为字典树,也叫做前缀树,叫字典树很形象,叫前缀树可以很好的区分,因为还有一种树叫做后缀树 自己就不瞎总结了,写估计也写不好.关键是时间不允许. 参考两个blog A   B 先给一个比较标准的模板 1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 5 using namespace std; 6 7 #defin

【字符串算法】字典树Trie入门

基本概念 顾名思义,字典树(也叫前缀树)就是可以像字典那样来保存一些单词的集合. 如图所示: (图片来自OIWiKi) 设根节点的标号为$0$,然后其余结点依次编号:我们用数组来存每个节点的所有子节点 更具体地,设数组$ch[MaxNode][SigmaSize]$,其中$MaxNode$表示最大可能的节点个数,$SigmaSize$是字符集合.$ch[i][j]$表示结点标号为$i$的结点 的 字母编号为$j$(比如说,j=字母-'a')的子节点的结点标号.如果$ch[i][j]$为$0$,则

字典树Trie树

一.字典树 字典树--Trie树,又称为前缀树(Prefix Tree).单词查找树或键树,是一种多叉树结构. 上图是一棵Trie树,表示了关键字集合{"a", "to", "tea", "ted", "ten", "i", "in", "inn"} .从上图可以归纳出Trie树的基本性质: 1. 根节点不包含字符,除根节点外的每一个子节点都包含一

数据结构——前缀树

Trie(前缀树/字典树) Trie,又经常叫前缀树,字典树等等,是一种多叉树结构.如下图: 基本功能实现: 只记录小写字母,用pass记录有多少字符串经过该节点,end记录有多少字符串以该节点结尾. 用数组实现: #include <iostream> #include <malloc.h> #include <string> using namespace std; typedef struct node{ int pass; int end; struct nod

高级数据结构:优先队列、图、前缀树、分段树以及树状数组详解

优秀的算法往往取决于你采用哪种数据结构,除了常规数据结构,日常更多也会遇到高级的数据结构,实现要比那些常用的数据结构要复杂得多,这些高级的数据结构能够让你在处理一些复杂问题的过程中多拥有一把利器.同时,掌握好它们的性质以及所适用的场合,在分析问题的时候回归本质,很多题目都能迎刃而解了. 这篇文章将重点介绍几种高级的数据结构,它们是:优先队列.图.前缀树.分段树以及树状数组. 一.优先队列 1.优先队列的作用 优先队列最大的作用是能保证每次取出的元素都是队列中优先级别最高的,这个优先级别可以是自定

【数据结构】前缀树/字典树/Trie

[前缀树] 用来保存一个映射(通常情况下 key 为字符串  value 为字符串所代表的信息) 例如:一个单词集合 words = {  apple, cat,  water  }   其中 key 为单词      value 代表该单词是否存在 words[ 'apple' ] = 存在     而     word[ ' abc' ] = 不存在 图示:一个保存了8个键的trie结构,"A", "to", "tea", "ted

【学习总结】数据结构-Trie/前缀树/字典树-及其最常见的操作

Trie/前缀树/字典树 Trie (发音为 "try") 或前缀树是一种树数据结构,用于检索字符串数据集中的键. 一种树形结构,是一种哈希树的变种. 典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计. 优点:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高. 应用: 自动补全 END 原文地址:https://www.cnblogs.com/anliux/p/12590368.html

常用算法之Trie【字典树,前缀树】

Trie中文名又叫做字典树,前缀树等,因为其结构独有的特点,经常被用来统计,排序,和保存大量的字符串,经常见于搜索提示,输入法文字关联等,当输入一个值,可以自动搜索出可能的选择.当没有完全匹配的结果时,可以返回前缀最为相似的可能. 其实腾讯的面试题有一个:如何匹配出拼写单词的正确拼写.其实用匹配树非常合适. 基本性质: 1.根节点不含有字符,其余各节点有且只有一个字符. 2.根节点到某一节点中经过的节点存储的值连接起来就是对应的字符串. 3.每一个节点所有的子节点的值都不应该相同. 借用一下维基