字典树(Trie tree)

来源:互联网 发布:北京网络安全员招聘网 编辑:程序博客网 时间:2024/04/30 20:58

字典树(Trie tree)

Trie,又称单词查找树键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。

性质

它有3个基本性质:

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

[编辑]图示

这是一个Trie结构的例子: Trie example.svg

在这个Trie结构中,保存了A、to、tea、ted、ten、i、in、inn这8个字符串,仅占用8个字节(不包括指针占用的空间)。

实例

这是一个用于词频统计的程序范例,因使用了getline(3),所以需要glibc才能链接成功,没有glibc的话可以自行改写。代码由User:JohnBull捐献,遵从GPL版权声明。

view plaincopy to clipboardprint?
  1. #include <stdio.h>   
  2. #include <stdlib.h>   
  3. #include <string.h>   
  4.     
  5. #define TREE_WIDTH 256   
  6.     
  7. #define WORDLENMAX 128   
  8.     
  9. struct trie_node_st {   
  10.         int count;   
  11.         struct trie_node_st *next[TREE_WIDTH];   
  12. };   
  13.     
  14. static struct trie_node_st root={0, {NULL}};   
  15.     
  16. static char *spaces=" \t\n/.\"\'()";   
  17.     
  18. static int  
  19. insert(const char *word)   
  20. {   
  21.         int i;   
  22.         struct trie_node_st *curr, *newnode;   
  23.     
  24.         if (word[0]=='\0') {   
  25.                 return 0;   
  26.         }   
  27.         curr = &root;   
  28.         for (i=0; ; ++i) {   
  29.                 if (curr->next[ word[i] ] == NULL) {   
  30.                         newnode=(struct trie_node_st*)malloc(sizeof(struct trie_node_st));   
  31.                         memset(newnode, 0, sizeof(struct trie_node_st));   
  32.                         curr->next[ word[i] ] = newnode;   
  33.                 }   
  34.                 if (word[i] == '\0') {   
  35.                         break;   
  36.                 }   
  37.                 curr = curr->next[ word[i] ];   
  38.         }   
  39.         curr->count ++;   
  40.     
  41.         return 0;   
  42. }   
  43.     
  44. static void  
  45. printword(const char *str, int n)   
  46. {   
  47.         printf("%s\t%d\n", str, n);   
  48. }   
  49.     
  50. static int  
  51. do_travel(struct trie_node_st *rootp)   
  52. {   
  53.         static char worddump[WORDLENMAX+1];   
  54.         static int pos=0;   
  55.         int i;   
  56.     
  57.         if (rootp == NULL) {   
  58.                 return 0;   
  59.         }   
  60.         if (rootp->count) {   
  61.                 worddump[pos]='\0';   
  62.                 printword(worddump, rootp->count);   
  63.         }   
  64.         for (i=0;i<TREE_WIDTH;++i) {   
  65.                 worddump[pos++]=i;   
  66.                 do_travel(rootp->next[i]);   
  67.                 pos--;   
  68.         }   
  69.         return 0;   
  70. }   
  71.     
  72. int  
  73. main(void)   
  74. {   
  75.         char *linebuf=NULL, *line, *word;   
  76.         size_t bufsize=0;   
  77.         int ret;   
  78.     
  79.         while (1) {   
  80.                 ret=getline(&linebuf, &bufsize, stdin);   
  81.                 if (ret==-1) {   
  82.                         break;   
  83.                 }   
  84.                 line=linebuf;   
  85.                 while (1) {   
  86.                         word = strsep(&line, spaces);   
  87.                         if (word==NULL) {   
  88.                                 break;   
  89.                         }   
  90.                         if (word[0]=='\0') {   
  91.                                 continue;   
  92.                         }   
  93.                         insert(word);   
  94.                 }   
  95.         }   
  96.     
  97. /* free(linebuf); */  
  98.     
  99.         do_travel(&root);   
  100.     
  101.         exit(0);   
  102. }  

在给一个例子:

view plaincopy to clipboardprint?
  1. #define MAX_NUM 26   
  2. enum NODE_TYPE{ //"COMPLETED" means a string is generated so far.  
  3.   COMPLETED,   
  4.   UNCOMPLETED   
  5. };   
  6. struct Node {   
  7.   enum NODE_TYPE type;   
  8.   char ch;   
  9.   struct Node* child[MAX_NUM]; //26-tree->a, b ,c, .....z  
  10. };   
  11.     
  12. struct Node* ROOT; //tree root  
  13.     
  14. struct Node* createNewNode(char ch){   
  15.   // create a new node   
  16.   struct Node *new_node = (struct Node*)malloc(sizeof(struct Node));   
  17.   new_node->ch = ch;   
  18.   new_node->type == UNCOMPLETED;   
  19.   int i;   
  20.   for(i = 0; i < MAX_NUM; i++)   
  21.     new_node->child[i] = NULL;   
  22.   return new_node;   
  23. }   
  24.     
  25. void initialization() {   
  26. //intiazation: creat an empty tree, with only a ROOT  
  27. ROOT = createNewNode(' ');   
  28. }   
  29.     
  30. int charToindex(char ch) { //a "char" maps to an index<br>  
  31. return ch - 'a';   
  32. }   
  33.     
  34. int find(const char chars[], int len) {   
  35.   struct Node* ptr = ROOT;   
  36.   int i = 0;   
  37.   while(i < len) {   
  38.    if(ptr->child[charToindex(chars[i])] == NULL) {   
  39.    break;   
  40.   }   
  41.   ptr = ptr->child[charToindex(chars[i])];   
  42.   i++;   
  43.   }   
  44.   return (i == len) && (ptr->type == COMPLETED);   
  45. }   
  46.     
  47. void insert(const char chars[], int len) {   
  48.   struct Node* ptr = ROOT;   
  49.   int i;   
  50.   for(i = 0; i < len; i++) {   
  51.    if(ptr->child[charToindex(chars[i])] == NULL) {   
  52.     ptr->child[charToindex(chars[i])] = createNewNode(chars[i]);   
  53.   }   
  54.   ptr = ptr->child[charToindex(chars[i])];   
  55. }   
  56.   ptr->type = COMPLETED;   
  57. }  

Trie树的基本实现

字母树的插入(Insert)、删除( Delete)和查找(Find)都非常简单,用一个一重循环即可,即第i 次循环找到前i 个字母所对应的子树,然后进行相应的操作。实现这棵字母树,我们用最常见的数组保存(静态开辟内存)即可,当然也可以开动态的指针类型(动态开辟内存)。至于结点对儿子的指向,一般有三种方法:

1、对每个结点开一个字母集大小的数组,对应的下标是儿子所表示的字母,内容则是这个儿子对应在大数组上的位置,即标号;

2、对每个结点挂一个链表,按一定顺序记录每个儿子是谁;

3、使用左儿子右兄弟表示法记录这棵树。

三种方法,各有特点。第一种易实现,但实际的空间要求较大;第二种,较易实现,空间要求相对较小,但比较费时;第三种,空间要求最小,但相对费时且不易写。

3、 Trie树的高级实现

可以采用双数组(Double-Array)实现。利用双数组可以大大减小内存使用量,具体实现细节见参考资料(5)(6)。

4、 Trie树的应用

Trie是一种非常简单高效的数据结构,但有大量的应用实例。

(1) 字符串检索

事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。

举例:

@  给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。

@  给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

(2)字符串最长公共前缀

Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。

举例:

@ 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少?

解决方案:首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。

而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;

2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;

(关于并查集,Tarjan算法,RMQ问题,网上有很多资料。)

(3)排序

Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。

举例:

@ 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。

(4) 作为其他数据结构和算法的辅助结构

如后缀树,AC自动机等

5、 Trie树复杂度分析

(1) 插入、查找的时间复杂度均为O(N),其中N为字符串长度。

(2) 空间复杂度是26^n级别的,非常庞大(可采用双数组实现改善)。

6、 总结

Trie树是一种非常重要的数据结构,它在信息检索,字符串匹配等领域有广泛的应用,同时,它也是很多算法和复杂数据结构的基础,如后缀树,AC自动机等,因此,掌握Trie树这种数据结构,对于一名IT人员,显得非常基础且必要!

7、 参考资料

(1)wiki:http://en.wikipedia.org/wiki/Trie

(2) 博文《字典树的简介及实现》:

http://hi.baidu.com/luyade1987/blog/item/2667811631106657f2de320a.html

(3) 论文《浅析字母树在信息学竞赛中的应用》

(4)  论文《Trie图的构建、活用与改进》

(5)  博文《An Implementation of Double-Array Trie》:

http://linux.thai.net/~thep/datrie/datrie.html

(6) 论文《An Efficient Implementation of Trie Structures》:

http://www.google.com.hk/url?sa=t&source=web&cd=4&ved=0CDEQFjAD&url=http%3A%2F%2Fciteseerx.ist.psu.edu%2Fviewdoc%2Fdownload%3Fdoi%3D10.1.1.14.8665%26rep%3Drep1%26type%3Dpdf&ei=qaehTZiyJ4u3cYuR_O4B&usg=AFQjCNF5icQbRO8_WKRd5lMh-eWFIty_fQ&sig2=xfqSGYHBKqOLXjdONIQNVw

7)刚刚分享了:"算法合集之《浅析字母树在信息学竞赛中的应用》.pdf" 下载链接:http://t.cn/zOlasAH http://t.cn/zOlasAQ