from:https://www.cnblogs.com/justinh/p/7716421.html
Trie,又经常叫前缀树,字典树等等。它有很多变种,如后缀树,Radix Tree/Trie,PATRICIA tree,以及bitwise版本的crit-bit tree。当然很多名字的意义其实有交叉。
定义
在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
trie中的键通常是字符串,但也可以是其它的结构。trie的算法可以很容易地修改为处理其它结构的有序序列,比如一串数字或者形状的排列。比如,bitwise trie中的键是一串位元,可以用于表示整数或者内存地址
基本性质
1,根节点不包含字符,除根节点意外每个节点只包含一个字符。
2,从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
3,每个节点的所有子节点包含的字符串不相同。
优点:
可以最大限度地减少无谓的字符串比较,故可以用于词频统计和大量字符串排序。
跟哈希表比较:
1,最坏情况时间复杂度比hash表好
2,没有冲突,除非一个key对应多个值(除key外的其他信息)
3,自带排序功能(类似Radix Sort),先序遍历trie可以得到排序。
缺点:
1,虽然不同单词共享前缀,但其实trie是一个以空间换时间的算法。其每一个字符都可能包含至多字符集大小数目的指针(不包含卫星数据)。
每个结点的子树的根节点的组织方式有几种。1>如果默认包含所有字符集,则查找速度快但浪费空间(特别是靠近树底部叶子)。2>如果用链接法(如左儿子右兄弟),则节省空间但查找需顺序(部分)遍历链表。3>alphabet reduction: 减少字符宽度以减少字母集个数。,4>对字符集使用bitmap,再配合链接法。
2,如果数据存储在外部存储器等较慢位置,Trie会较hash速度慢(hash访问O(1)次外存,Trie访问O(树高))。
3,长的浮点数等会让链变得很长。可用bitwise trie改进。
bit-wise Trie
类似于普通的Trie,但是字符集为一个bit位,所以孩子也只有两个。
可用于地址分配,路由管理等。
虽然是按bit位存储和判断,但因为cache-local和可高度并行,所以性能很高。跟红黑树比,红黑树虽然纸面性能更高,但是因为cache不友好和串行运行多,瓶颈在存储访问延迟而不是CPU速度。
压缩Trie
压缩分支条件:
1,Trie基本不变
2,只是查询
3,key跟结点的特定数据无关
4,分支很稀疏
若允许添加和删除,就可能需要分裂和合并结点。此时可能需要对压缩率和更新(裂,并)频率进行折中。
外存Trie
某些变种如后缀树适合存储在外部,另外还有B-trie等。
应用场景:
(1) 字符串检索
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
举例:
1,给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
2,给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。
3,1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。
(2)文本预测、自动完成,see also,拼写检查
(3)词频统计
1,有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
2,一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
3,寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复度比较高,虽然总数是1千万,但是如果去除重复,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
(1) 请描述你解决这个问题的思路;
(2) 请给出主要的处理流程,算法,以及算法的复杂度。
==》若无内存限制:Trie + “k-大/小根堆”(k为要找到的数目)。
否则,先hash分段再对每一个段用hash(另一个hash函数)统计词频,再要么利用归并排序的某些特性(如partial_sort),要么利用某使用外存的方法。参考
“http://www.dataguru.cn/thread-485388-1-1.html。
“算法面试题之统计词频前k大” http://blog.csdn.net/u011077606/article/details/42640867
(4)排序
Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。
比如给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。
(5)字符串最长公共前缀
Trie树利用多个字符串的公共前缀来节省存储空间,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。
举例:
给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少?
解决方案:首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。
而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:
1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;
2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;
(6)字符串搜索的前缀匹配
trie树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。
Trie树检索的时间复杂度可以做到n,n是要检索单词的长度,
如果使用暴力检索,需要指数级O(n2)的时间复杂度。
(7) 作为其他数据结构和算法的辅助结构
如后缀树,AC自动机等
后缀树可以用于全文搜索
转一篇关于几种Trie速度比较的文章:http://www.hankcs.com/nlp/performance-comparison-of-several-trie-tree.html
Trie树和其它数据结构的比较 http://www.raychase.net/1783
参考:
[1] 维基百科:Trie, https://en.wikipedia.org/wiki/Trie
[2] LeetCode字典树(Trie)总结, http://www.jianshu.com/p/bbfe4874f66f
[3] 字典树(Trie树)的实现及应用, http://www.cnblogs.com/binyue/p/3771040.html#undefined
[4] 6天通吃树结构—— 第五天 Trie树, http://www.cnblogs.com/huangxincheng/archive/2012/11/25/2788268.html
在Trie树中主要有3个操作,插入、查找和删除。一般情况下Trie树中很少存在删除单独某个结点的情况,因此只考虑删除整棵树。
1 #include <stdio.h> 2 #include <string.h> 3 #include <algorithm> 4 5 using namespace std; 6 #define maxn 26 7 struct Trie{ 8 int Flag;// 通过该节点单词的数量,可用来统计前缀 9 bool End;// 判断该节点是否是是单词的结尾 10 char str[20];//如果该节点是单词结尾(End=true) 可存放整个单词 11 Trie *Next[maxn]; 12 Trie(){ //结构体初始化 13 Flag=1; 14 End=false; 15 memset(Next,0,sizeof(Next)); 16 } 17 }*Root; 18 19 void Insert(char *str) //插入操作 20 { 21 Trie *p=Root; 22 Trie *q=NULL; 23 int len=strlen(str); 24 for(int i=0;i<len;i++) 25 { 26 int key=str[i]-'a'; //数字的话为 str[i]-'0' 27 if(!p->Next[key]) 28 { 29 q=new Trie(); //不能找到,说明该key没有在这条支路,新建节点 30 p->Next[key]=q; 31 p=p->Next[key]; 32 } 33 else 34 { 35 p=p->Next[key]; 36 p->Flag++; //能查到,则通过该节点(key)的次数加1 37 } 38 if(i==len-1)//到了字符串的最后一个 39 { 40 p->End=true; 41 strcpy(p->str,str); 42 } 43 } 44 } 45 46 int Qurey(char *str) //查询操作,根据查询的内容,选int bool void 47 { 48 int len=strlen(str); 49 Trie *p=Root; 50 for(int i=0;i<len;i++) 51 { 52 int key=str[i]-'a'; 53 if(!p->Next[key]) 54 return 0; 55 p=p->Next[key]; 56 } 57 return p->Flag; 58 } 59 60 void Free(Trie* T) //释放字典树 61 { 62 if(T==NULL) 63 return; 64 for(int i=0;i<maxn;i++) 65 { 66 if(T->Next[i]) 67 Free(T->Next[i]); //递归的方式 68 } 69 delete(T); 70 } 71 72 int main() 73 { 74 Root=new Trie(); 75 // char str[20]; 76 // gets(str); 77 // Insert(str); 78 // printf("%d",Qurey(str)); 79 Free(Root); 80 return 0; 81 }
也可以用二维数组来表示字典树,用结构体超时的话,可以试试数组
1 #include <stdio.h> 2 #include <string.h> 3 #include <algorithm> 4 5 using namespace std; 6 #define maxn 26 7 const int MAXN=2e6+5; 8 int Trie[MAXN][maxn]; //用一个二维数组来表示树 9 int Count[MAXN]; //记录频数 10 bool End[MAXN]; //结尾标志 11 int tot; //总结点数 12 13 void Insert(char *str) // 插入操作 14 { 15 int Root=0; 16 int len=strlen(str); 17 for(int i=0;i<len;i++) 18 { 19 int key=str[i]-'a'; 20 if(!Trie[Root][key]) 21 { 22 Trie[Root][key]=++tot;//相当于建立新节点 23 Root=Trie[Root][key]; 24 Count[Root]=1; 25 } 26 else 27 { 28 Root=Trie[Root][key]; 29 Count[Root]++; 30 } 31 } 32 End[Root]=true; 33 } 34 35 bool Qurey(char *str) 36 { 37 int Root=0; 38 int len=strlen(str); 39 for(int i=0;i<len;i++) 40 { 41 int key=str[i]-'a'; 42 if(!Trie[Root][key]) 43 return false; 44 Root=Trie[Root][key]; 45 } 46 return true; 47 } 48 49 void Clear() 50 { 51 for(int i=0;i<=tot;i++) //注意是<=tot 52 { 53 End[i]=false; 54 Count[i]=0; 55 for(int j=0;j<maxn;j++) 56 { 57 Trie[i][j]=0; 58 } 59 } 60 tot=0; 61 } 62 63 int main() 64 { 65 // char str[20]; 66 // gets(str); 67 // Insert(str); 68 // if(Qurey(str)) 69 // printf("Yes\n"); 70 // Clear(); 71 return 0; 72 }
例题及思路
Problem Description
Input
输入数据的第一部分是一张单词表,每行一个单词,单词的长度不超过10,它们代表的是老师交给Ignatius统计的单词,一个空行代表单词表的结束.第二部分是一连串的提问,每行一个提问,每个提问都是一个字符串.
注意:本题只有一组测试数据,处理到文件结束.
Output
对于每个提问,给出以该字符串为前缀的单词的数量.
Sample Input
banana
band
bee
absolute
acm
ba
b
band
abc
Sample Output
2 3 1 0
统计出以某个字符串为前缀的单词数量,首先构建出trie树并记录每个节点的访问次数,然后在上面查询就好了,模板题。
1 #include <cstdio> 2 #include <cstring> 3 #include <cstdlib> 4 #include <algorithm> 5 #define MAXN 26 6 using namespace std; 7 8 struct Trie 9 { 10 Trie *Next[MAXN]; 11 int Flag; 12 Trie() 13 { 14 Flag=1; 15 memset(Next,NULL,sizeof(Next)); 16 } 17 }; 18 19 struct Trie* Root; 20 21 void Insert(char* str) 22 { 23 Trie *p,*q; 24 p=Root; 25 int len=strlen(str); 26 for(int i=0;i<len;++i) 27 { 28 int key=str[i]-'a'; 29 if(p->Next[key]==NULL) 30 { 31 q=new Trie(); 32 p->Next[key]=q; 33 p=p->Next[key]; 34 } 35 else 36 { 37 p=p->Next[key]; 38 ++p->Flag; 39 } 40 } 41 } 42 43 int Qurey(char *str) 44 { 45 int len=strlen(str); 46 Trie* p=Root; 47 for(int i=0;i<len;++i) 48 { 49 int key=str[i]-'a'; 50 if(p->Next[key]==NULL) 51 return 0; 52 p=p->Next[key]; 53 } 54 return p->Flag; 55 56 } 57 58 void Free(Trie* T) 59 { 60 if(T==NULL) return; 61 for(int i=0;i<MAXN;i++) 62 { 63 if(T->Next[i]) Free(T->Next[i]); 64 } 65 delete(T); 66 } 67 68 int main() 69 { 70 //freopen("sample.txt","r",stdin); 71 char str[15]; 72 Root=new Trie(); 73 while(*gets(str)) //效果等同于while(gets(str)&&str[0]!=0) 74 { 75 Insert(str); 76 } 77 while(~scanf("%s",str)) 78 { 79 printf("%d\n",Qurey(str)); 80 } 81 Free(Root); 82 return 0; 83 }