前言
今天具体分析一下trie树,包括:原理分析,应用场合,复杂度分析,与hash的比较,源码展现。大部分内容来自互联网,文中会注明出处。
原理分析
主要是hash树的变种,先看下图:
每一个点存储一个字符,所以trie(字典树)的key不是每个字符串,而是一条链。其原理就是充分利用了公共字符串,这样在查找时,就不需要做重复工作了。并且查找的复杂度可以维持在O(len),len为字符串的长度,原因很简单,我们只需沿着从根到节点的一条路径就可以了。插入也是类似的原理。
建立的过程:
每个节点包括三个信息:26个指针(假设查询26个英文小写字母),每个节点的后继节点可能出现26个字母当中的任何一个,故需26个指针,当然对于不存在的后继结点,设置为NULL;标志位,此标志位主要是为了识别是否为字符串为一个单词;第三个为附加信息,看具体应用场合,可以为字符出现的次数,也可以为前缀的个数,字符串的个数,总之灵活应用就是。
查询的过程:
与建立过程原理雷同,只是没有创建新节点的过程;
删除的过程:
很少见,如果非要删除,则采用递归从下往上挨个delete即可;
应用场合
我直接转载:http://www.cnblogs.com/aiyelinglong/archive/2012/04/09/2439777.html
trie树的应用:
1.有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
2.1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?
3.一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
4.寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
后缀树的应用:
1.查找字符串O是否在字符串S中。
方案:用S构造后缀树,按在trie中搜索字串的方法搜索O即可。
原理:若O在S中,则O必然是S的某个后缀的前缀。
例如:leconte,查找O:con是否在S中,则O(con)必然是S(leconte)的前缀。
2.指定字符串T在字符串S中的重复次数。
方案:用S+’$’构造后缀树,搜索T节点下的叶子节点数目即为重复次数
原理:如果T在S中重复了两次,则S应有两个后缀以T为前缀,重复次数自然统计出来了。
3.字符串S中的最长重复子串
方案:原理同2,具体做法是找到最深的非叶子节点。
这个深指从root所经历过的字符个数,最深非叶子节点所经历的字符串起来就是最长重复子串。为什么非要是叶子节点呢?因为既然是要重复的,当然叶子节点个数要>=2
4.两个字符串S1,S2的最长公共子串(而非以前所说的最长公共子序列,因为子序列是不连续的,而子串是连续的。)
方案:将S1#S2$作为字符串压入后缀树,找到最深的非叶子节点,且该节点的叶子节点既有#也有$.
5.最长回文子串
复杂度分析
前文已经提及,建立的时间复杂度为:O(n*len),查询,插入都为O(len)。空间复杂度就比较大了,这也是它的一个缺点,主要是指针得占用空间。
与hash的比较
首先比较创建的复杂度,创建的复杂度,hash为O(n*(len+3))(n指字符串的个数,len指字符串的长度),原理可见我的博文hash 一个海量数据的实现,里面有段代码:
int SDBMHash(char* str)
{
int hash = 0;
while(*str!='\0')
{
hash = *str++ + (hash <<6) + (hash <<16) - hash;
}
return (hash & 0x7FFFFFFF);
}
分析:3具体指int hash = 0; 和return (hash & 0x7FFFFFFF);有人会说,这也算,几乎没影响,但是大家想想,每个字符串多俩次操作,当字符串很大时,就不是俩次的问题了可能是10的几次方了,还有一次是hash表的操作。查询和插入同样的道理,每个字符串多两个操作。所以hash的时间复杂度不如trie的。这还是小case,在很多方面hash没法跟trie比的,比如查找前缀字符串,trie几乎用不到O(len),hash的操作就复杂多了,并且前缀字符串还要额外的hashmap。空间方面,可能hash 节省,但是恰恰就是因为trie牺牲了空间才换如此巨大的时间效果。
源码展现
我自己创建了一个txt文件,里面有很多单词,一行一个,利用trie统计某个单词出现的频数,可在我的资源文件里下到工程文件,里面有一个txt。可以在txt里复制同一个单词多次,然后查询,就可以看到它存在的次数了。
#include<iostream>
#include<cstring>
#include<fstream>
using namespace std;
const int n=26;
typedef struct Trie_node
{
int count; // 统计单词前缀出现的次数
struct Trie_node* next[n]; // 指向各个子树的指针
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;// 单词结束的地方标记此处可以构成一个单词
node->count++;
}
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)
{
cout<<endl<<word<<"在文件中不存在";
return 0;
}
}
if(node->exist==true)
cout<<endl<<word<<"出现了"<<node->count<<"次";
return node->count;
}
const int num=5000;
//产生一个txt文件,模拟字符串
void createStrTXT()
{
for(int i=0;i<num;++i)
{
char temp[12]={'\n','\r',rand()%26+97,rand()%26+97,rand()%26+97,rand()%26+97,rand()%26+97,rand()%26+97,rand()%26+97,rand()%26+97,rand()%26+97,'\0'};
char*str=temp;
ofstream ofs("str.txt",ios::app);
ofs<<str;
}
}
void establishTrieTree(Trie root)
{
ifstream ifs("str.txt");
char str[10];
int i=0;
while(ifs>>str)
{
Trie_insert(root,str);
cout<<"插入单词:"<<str<<endl;
i++;
}
cout<<"总共插入"<<i<<"个单词";
}
int main(void)
{
//初始化root
Trie root=createTrieNode();
//createStrTXT();
establishTrieTree( root);
Trie_search(root,"zxuglsdsm");
return 0;
}
测试图: