l Trie原理
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
l Trie性质
好多人说trie的根节点不包含任何字符信息,我所习惯的trie根节点却是包含信息的,而且认为这样也方便,下面说一下它的性质 (基于本文所讨论的简单trie树)
1. 字符的种数决定每个节点的出度,即branch数组(空间换时间思想)
2. branch数组的下标代表字符相对于a的相对位置
3. 采用标记的方法确定是否为字符串。
4. 插入、查找的复杂度均为O(len),len为字符串长度
l Trie的示意图
如图所示,该trie树存有abc、d、da、dda四个字符串,如果是字符串会在节点的尾部进行标记。没有后续字符的branch分支指向NULL
l TrieTrie的优点举例
已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个串为另一个串的前缀子串。下面对比3种方法:
1. 最容易想到的:即从字符串集中从头往后搜,看每个字符串是否为字符串集中某个字符串的前缀,复杂度为O(n^2)。
2. 使用hash:我们用hash存下所有字符串的所有的前缀子串。建立存有子串hash的复杂度为O(n*len)。查询的复杂度为O(n)* O(1)= O(n)。
3. 使用trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以b,c,d....等不是以a开头的字符串就不用查找了。所以建立trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度只是O(len)。
解释一下hash为什么不能将建立与查询同时执行,例如有串:911,911456输入,如果要同时执行建立与查询,过程就是查询911,没有,然后存入9、91、911,查询911456,没有然后存入9114、91145、911456,而程序没有记忆功能,并不知道911在输入数据中出现过。所以用hash必须先存入所有子串,然后for循环查询。
而trie树便可以,存入911后,已经记录911为出现的字符串,在存入911456的过程中就能发现而输出答案;倒过来亦可以,先存入911456,在存入911时,当指针指向最后一个1时,程序会发现这个1已经存在,说明911必定是某个字符串的前缀,该思想是我在做pku上的3630中发现的,详见本文配套的“入门练习”。
搞了几个小时,看来以后数字的Hash还是少用Trie为妙..
l Trie的简单实现(插入、查询)
模板
/* 字典树,又称单词查找树,Trie树,是一种树形结构,典型应用是用于统计,排序和保存大量的字符串,所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来节约存储空间,最大限度的减少无谓的字符串比较,查询效率比哈希表高。 字典树的应用: 字符串的快速检索 哈希 最长公共前缀 */ //Trie树模板 by AbandonZHANG struct Trie_node { int Ch_Count; //统计单词个数,如果为0表示没有该串。 int hash; //单词hash后的标识数 Trie_node *next[26];//指向各个子树的指针,下标0-25代表26字符,如果是数字改成10就行了 Trie_node() { Ch_Count=0; hash=-1; memset(next,NULL,sizeof(next)); } }; class Trie { public: Trie(); void insert(char *word); //插入新单词 int search(char * word); //返回单词个数,如果为0表示不存在该单词 private: Trie_node* root; }; Trie::Trie() { root = new Trie_node(); } void Trie::insert(char * word) //有时候可以插入查询一块儿做 { Trie_node *p = root; int len=strlen(word); if(len==0)return ; for (int i=0;i<len;i++) { if(p->next[word[i]-'a'] == NULL) //如果不存在的话,我们就建立一个新的节点 { Trie_node *tmp = new Trie_node(); p->next[word[i]-'a'] = tmp; p = p->next[word[i]-'a']; //每插入一步,相当于有一个新串经过,指针要向下移动 } else //如果这个节点之前就已经存在呃,我们只需要把统计次数加上1 p=p->next[word[i]-'a']; //p->Ch_Count++; //这里是求所有前缀出现的次数,如果只求整个单词出现次数则用后一个 } p->Ch_Count++; //求整个单词的出现次数 /* if (p->hash<0) p->hash=words_num++; */ return ; } int Trie::search(char * word) //返回单词个数,如果为0表示不存在该单词 { Trie_node *p = root; int len=strlen(word); if (len==0) return 0; for (int i=0;i<len;i++) { if (p->next[word[i]-'a']!=NULL) p=p->next[word[i]-'a']; else return 0; } return p->Ch_Count; //return p->hash; //返回单词的hash标识数,如果为0表示不存在该单词 } int main() //简单测试 { Trie t; t.insert("a"); t.insert("abandon"); string c= "abandoned"; t.insert(c); t.insert("abashed"); if(t.search("abashed")) printf("abashed true\n"); if(t.search("a")) printf("a true\n"); if(t.search("abandoned")) printf("abandoned true\n"); if(t.search("aba")) //看前缀统计不统计 printf("aba true\n"); return 0; }
入门练习 :PKU POJ 3630解题报告