作者:July、yansha。
出处:http://blog.csdn.net/v_JULY_v 。
1.Trie树
1)简述
又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。
3个基本性质:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
假设有b,abc,abd,bcd,abcd,efg,hii 这6个单词。
好比大海搜人,立马就能确定东南西北中的到底哪个方位,如此迅速缩小查找的范围和提高查找的针对性。
那么,对于一个单词,我只要顺着他从根走到对应的节点,再看这个节点是否被标记为红色就可以知道它是否出现过了。把这个节点标记为红色,就相当于插入了这个单词。
2)比较
已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个串为另一个串的前缀子串。下面对比3种方法:
- 最容易想到的:即从字符串集中从头往后搜,看每个字符串是否为字符串集中某个字符串的前缀,复杂度为O(n^2)。
- 使用hash:我们用hash存下所有字符串的所有的前缀子串,建立存有子串hash的复杂度为O(n*len),而查询的复杂度为O(n)* O(1)= O(n)。
- 使用trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以b,c,d....等不是以a开头的字符串就不用查找了。所以建立trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度也只是O(len)。(说白了,就是Trie树的平均高度h为len,所以Trie树的查询复杂度为O(h)=O(len)。好比一棵二叉平衡树的高度为logN,则其查询,插入的平均时间复杂度亦为O(logN))。
下面解释下上述方法3中所说的为什么hash不能将建立与查询同时执行,而Trie树却可以:
- 在hash中,例如现在要输入两个串911,911456,如果要同时查询这两个串,且查询串的同时若hash中没有则存入。那么,这个查询与建立的过程就是先查询其中一个串911,没有,然后存入9、91、911;而后查询第二个串911456,没有然后存入9、91、911、9114、91145、911456。因为程序没有记忆功能,所以并不知道911在输入数据中出现过,只是照常以例行事,存入9、91、911、9114、911...。也就是说用hash必须先存入所有子串,然后for循环查询。
- 而trie树中,存入911后,已经记录911为出现的字符串,在存入911456的过程中就能发现而输出答案;倒过来亦可以,先存入911456,在存入911时,当指针指向最后一个1时,程序会发现这个1已经存在,说明911必定是某个字符串的前缀。
3)代码实现

1 // trie tree.cpp : 定义控制台应用程序的入口点。 2 // 3 #include "stdafx.h" 4 //功能:统计一段英文的单词频率(文章以空格分隔,没有标点) 5 //思路:trie节点保存单词频率,然后通过DFS按字典序输出词频 6 //时空复杂度: O(n*len)(len为单词平均长度) 7 //copyright@yansha 2011.10.25 8 //updated@July 2011.10.26 9 //程序尚不完善,有很多地方还需改进。 10 #include <stdio.h> 11 #include <stdlib.h> 12 #include <string.h> 13 #include <ctype.h> 14 #include <assert.h> 15 16 #define num_of_letters 26 17 #define max_word_length 20 18 19 // 定义trie树节点 20 struct Trie 21 { 22 int count; 23 Trie *next[num_of_letters]; 24 }; 25 26 // 定义根节点 27 Trie *root = NULL; 28 29 /** 30 * 建立trie树,同时保存单词频率 31 */ 32 void create_trie(char *word) 33 { 34 int len = strlen(word); 35 Trie *cur = root, *node; 36 int pos = 0; 37 38 // 深度为单词长度 39 for(int i = 0; i < len; ++i) 40 { 41 // 将字母范围映射到0-25之间 42 pos = word[i] - 'a'; 43 44 // 如果当前字母没有对应的trie树节点则建立,否则处理下一个字母 45 if(cur->next[pos] == NULL) //1、这里应该有个查找过程 46 { 47 node = (Trie *)malloc(sizeof(Trie)); 48 node->count = 0; 49 50 // 初始化next节点 51 for(int j = 0; j < num_of_letters; ++j) 52 node->next[j] = NULL; 53 54 // 开始处理下一个字母 55 cur->next[pos] = node; 56 } 57 cur = cur->next[pos]; 58 } 59 // 单词频率加1 60 cur->count++; 61 } 62 63 /** 64 * 大写字母转化成小写字母 65 */ 66 void upper_to_lower(char *word, int len) 67 { 68 for (int i = 0; i < len; ++i) 69 { 70 if(word[i] >= 'A' && word[i] <= 'Z') 71 word[i] += 32; 72 } 73 } 74 75 /** 76 * 处理输入 77 */ 78 void process_input() 79 { 80 char word[max_word_length]; 81 82 // 打开统计文件(注意保持文件名一致) 83 FILE *fp_passage = fopen("passage.txt", "r"); 84 assert(fp_passage); 85 86 // 循环处理单词 87 while (fscanf(fp_passage, "%s", word) != EOF) 88 { 89 int len = strlen(word); 90 if (len > 0) 91 upper_to_lower(word, len); 92 create_trie(word); 93 } 94 fclose(fp_passage); 95 } 96 97 /** 98 * 深度优先遍历 99 */ 100 void trie_dfs(Trie *p, char *queue) 101 { 102 for(int i = 0; i < num_of_letters; ++i) 103 { 104 if(p->next[i] != NULL) 105 { 106 // 定义队列头结点 107 char *head = queue; 108 109 // 在末尾增加一个字母 110 while (*queue != '