Trie 的强大之处就在于它的时间复杂度。它的插入和查询时间复杂度都为 O(k) ,其中 k 为 key 的长度,与
Trie 中保存了多少个元素无关。Hash 表号称是 O(1) 的,但在计算 hash 的时候就肯定会是 O(k) ,而且还有碰撞之类的问题;Trie 的缺点是空间消耗很高。
Trie树特性:
1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3)每个节点的所有子节点包含的字符都不相同。
4)如果字符的种数为n,则每个结点的出度为n,这也是空间换时间的体现,浪费了很多的空间。
5)插入查找的复杂度为O(n),n为字符串长度。
已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个串为另一个串的前缀子串。
下面对比3种方法:
1、 最容易想到的:即从字符串集中从头往后搜,看每个字符串是否为字符串集中某个字符串的前缀,复杂度为O(n^2)。3、 使用Trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以b、c、d....等不是以a开头的字符串就不用查找了,这样迅速缩小查找的范围和提高查找的针对性。所以建立Trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度只是O(len)。
删除可以以递归的形式进行删除。
#include<iostream> #include<cstring> using namespace std; typedef struct Trie_node { int count; // 统计单词前缀出现的次数 struct Trie_node* next[26]; // 指向各个子树的指针 bool exist; // 标记该结点处是否构成单词 }TrieNode , *Trie; TrieNode* createTrieNode() { TrieNode* node = (TrieNode *)malloc(sizeof(TrieNode)); node->count = 0; node->exist = false; memset(node->next , 0 , sizeof(node->next)); // 初始化为空指针 return node; } void Trie_insert(Trie root, char* word) { Trie node = root; char *p = word; int id; while( *p ) { id = *p - 'a'; if(node->next[id] == NULL) { node->next[id] = createTrieNode(); } node = node->next[id]; // 每插入一步,相当于有一个新串经过,指针向下移动 ++p; node->count += 1; // 这行代码用于统计每个单词前缀出现的次数(也包括统计每个单词出现的次数) } node->exist = true; // 单词结束的地方标记此处可以构成一个单词 } int Trie_search(Trie root, char* word) { Trie node = root; char *p = word; int id; while( *p ) { id = *p - 'a'; node = node->next[id]; ++p; if(node == NULL) return 0; } return node->count; } int main(void) { Trie root = createTrieNode(); // 初始化字典树的根节点 char str[12] ; bool flag = false; while(gets(str)) { if(flag) printf("%d\n",Trie_search(root , str)); else { if(strlen(str) != 0) { Trie_insert(root , str); } else flag = true; } } return 0; }
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
举例:
1)有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
2)给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
3)给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。
4)1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串
5)寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。举例:
1) 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少. 解决方案:
首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线 (Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。
而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:
1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;
2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;
Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。
举例: 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。
如后缀树,AC自动机等。
3、 Trie树的高级实现
可以采用双数组(Double-Array)实现。利用双数组可以大大减小内存使用量
(5) 博文《An Implementation of Double-Array Trie》:
http://linux.thai.net/~thep/datrie/datrie.html
(6) 论文《An Efficient Implementation of Trie Structures》:
原文地址:http://blog.csdn.net/yusiguyuan/article/details/42715617