转
一、入门介绍:
1、 Trie树(字典树)定义:
又称单词查找树,Trie 树,是一种树形结构,是一种哈希树的变种。
或者按顺序输入 at,cash,app,apple,aply,ok 建一颗字典树
2:应用:
典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计
3:优缺点:
它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
他的缺点是:Trie树的内存消耗非常大.当然,或许用左儿子右兄弟的方法建树的话,可能会好点
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的
4:基本性质:
1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2) 字典树用边表示字母
3)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
4)每个节点的所有子节点包含的字符都不相同,每个节点最多有26个子节点(在单词只包含小写字母的情况下)
5) 有相同前缀的单词公用前缀节点
6) 整棵树的根节点是空的,便于插入和查找
7) 每个单词结束的时候用一个特殊字符表示,那么从根节点到任意一个特殊字符,所经过的边的所有字母表示一个单词
二、基本操作(以下操作只假设有小写字母)
1、插入操作:insert()
(1)、思路
从左到右扫这个单词,如果字母在相应根节点下没有出现过,就插入这个字母;否则沿着字典树往下走,看单词的下一个字母。
这就产生一个问题:往哪儿插?计算机不会自己选择位置插,我们需要给它指定一个位置,那就需要给每个字母编号。
(2)、解决方案:
我们设数组 trie[ i ][ j ]= k,表示编号为 i 的节点的第 j 个孩子是编号为 k 的节点。
解释一下:
这里有2种编号,一种是 i,k表示节点的位置编号,这是相对整棵树而言的;另一种是 j,表示节点 i 的第 j 个的孩子,这是相对节点 i 而言的。
po图理解一下:
还是单词 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
(3)、插入CODE
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];//顺着字典树往下走
}
}
2、查找操作:Search()
查找有很多种,可以查找某一个前缀,也可以查找整个单词。
我们以查找一个前缀是否出现过为例讲解
(1)、查找字母
从左往右以此扫描每个字母,顺着字典树往下找,能找到这个字母,往下走,否则结束查找,即没有这个前缀;前缀扫完了,表示有这个前缀。
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;//找到了
}
(2)、查找单词
我们用bool变量 v[i] 表示节点 i 是否是单词结束的标志。
那么最后 return的是 v[root]
所以在插入操作中插入完每个单词,要对单词最后一个字母的 v[i]置为true,其他的都是false
(3)、查找前缀出现的次数
用一个sum[],表示位置 i 被访问过的次数
那么最后 return的是 sum[root],插入操作中每访问一个节点,都要让他的sum++
::这里前缀的次数是标记在前缀的最后一个字母所在位置的后一个位置上
比如:前缀abc出现的次数标记在c所在位置的后一个位置上,
三:CODE
//对于字符串比较多的要统计个数的,map被卡的情况下,直接用字典树
//很多题都是要用到节点下标来表示某个字符串
const int maxn =2e6+5;//如果是64MB可以开到2e6+5,尽量开大
int tree[maxn][30];//tree[i][j]表示节点i的第j个儿子的节点编号
bool flagg[maxn];//表示以该节点结尾是一个单词
int tot;//总节点数
void insert_(char *str)
{
int len=strlen(str);
int root=0;
for(int i=0;i<len;i++)
{
int id=str[i]-'0';
if(!tree[root][id]) tree[root][id]=++tot;
root=tree[root][id];
}
flagg[root]=true;
}
bool find_(char *str)//查询操作,按具体要求改动
{
int len=strlen(str);
int root=0;
for(int i=0;i<len;i++)
{
int id=str[i]-'0';
if(!tree[root][id]) return false;
root=tree[root][id];
}
return true;
}
void init()//最后清空,节省时间
{
for(int i=0;i<=tot;i++)
{
flagg[i]=false;
for(int j=0;j<10;j++)
tree[i][j]=0;
}
tot=0;//RE有可能是这里的问题
}
3、排序操作
有时,我们会碰到对字符串的排序,若采用一些经典的排序算法,则时间复杂度一般为O(n*lgn),但若采用Trie树,则时间复杂度仅为O(n)。
Trie树又名字典树,从字面意思即可理解,这种树的结构像英文字典一样,相邻的单词一般前缀相同,之所以时间复杂度低,是因为其采用了以空间换取时间的策略。
下图为一个针对字符串排序的Trie树(我们假设在这里字符串都是小写字母),每个结点有26个分支,每个分支代表一个字母,结点存放的是从root节点到达此结点的路经上的字符组成的字符串。
将每个字符串插入到trie树中,到达特定的结尾节点时,在这个节点上进行标记,如插入"afb",第一个字母为a,沿着a往下,然后第二个字母为f,沿着f往下,第三个为b,沿着b往下,由于字符串最后一个字符为'\0',因而结束,不再往下了,然后在这个节点上标记afb.count++,即其个数增加1.
之后,通过前序遍历此树,即可得到字符串从小到大的顺序。