字典树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
常见的可以使用字典树解决的问题举例:
1、已知N个单词,对于每一个单词,判断它有没有出现过,如果出现了,求第一次出现在第几个位置。
2、已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个串为另一个串的前缀子串。
3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M,返回频数最高的100个词。
4、1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。
5、一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词。
6、给定N个互不相同的仅由一个单词构成的英文名,让你将他们按字典序从小到大输出。
上面的问题都可以通过建一个字典树解决,将字符串分别插入到字典树中,插入的同时可以进行查询,这样就可以知道该字符串有没有出现过。字典树结点可以添加一些附加信息,比如该结点代表的字符出现的次数,该结点是不是某个字符串的结尾以及该字符串出现的次数等。另外,对字典树进行先序遍历可以输出字符串的排序。
Tire树的结构如下图所示:
每个节点代表一个字符,从根节点向下遍历可以得到所有字符串的前缀,遇到结尾字符(红色)就得到一个字符串。
下面是我写的字典树简单实现,该字典树不仅保存了每个字符出现的次数(成员变量_count),该字典树中字符串的出现次数(成员变量_isEnd)。在插入字符串的每个字符的时候需要更新_count,插入完一个字符串后更新_isEnd。最后,删除一个字符串需要注意的是先判断是否存在待删除的字符串,如果存在,那么应该从下而上判断和删除节点(如果节点的计数为1,那么就释放内存,否则减小_count)。
/*实现一个字典树*/
#include <iostream>
#include <cstring>
#include <stack>
struct TrieNode
{
enum { MAX = 26 };
int _isEnd;//表示字符串的结尾,_isEnd可以表示字符出现次数
int _count;//字符出现的次数
TireNode* children[MAX];//指针数组
TireNode() : _isEnd(0), _count(1)
{
memset(children, NULL, sizeof(children));
}
};
TrieNode* create()
{
return new TireNode;
}
void insert(TrieNode* root, const char* str)
{
if (NULL == root || NULL == str || '\0' == *str)
{
return;
}
TrieNode* p = root;
while (*str != '\0')
{
if (NULL == p->children[*str - 'a']) //结点不存在
{
p->children[*str - 'a'] = new TrieNode;
}
else
{
p->children[*str - 'a']->_count++;//更新字符结点计数
}
p = p->children[*str - 'a'];
++str;
}
p->_isEnd++;//插完一个字符串更新其出现的次数
}
//字典树root是否包含字符串str, 如果isPrefix == true,那么str是root中某个字符串的一个前缀
bool contains(TrieNode* root, const char* str, bool& isPrefix)
{
if (NULL == root || NULL == str)
{
return false;
}
if ('\0' == *str)
{
return true;
}
TrieNode* p = root;
while (p != NULL && *str != '\0')
{
p = p->children[*str - 'a']; //p指向str的当前字符
++str;//str跳到下一个字符
}
//当*str == '\0'的时候,p指向str的最后一个有效字符(不是'\0')
isPrefix = false;
if (p != NULL)
{
isPrefix = true;
if (p->_isEnd > 0)//字符串结束符标记
{
return true;
}
}
return false;
}
//从字典树中删除字符串str,如果不存在就返回false,成功删除就返回true
bool remove(TrieNode* root, const char* str)
{
if (NULL == root || NULL == str)
{
return false;
}
if ('\0' == *str)
{
return true;
}
std::stack<TrieNode**> s;//结点只能从下向上删除,否则会破坏树结构,释放内存后要置NULL,故保存指针的地址!!!
TrieNode* p = root;
while (p != NULL && *str != '\0')
{
if ((p->children[*str - 'a']) != NULL)
{
s.push(&(p->children[*str - 'a']));//压入指针地址,很重要!
}
p = p->children[*str - 'a'];
++str;//str跳到下一个字符
}
if (p != NULL && p->_isEnd > 0) //存在字符串str
{
TrieNode** end = s.top();//代表待删除的字符串的最后一个字符的节点
(*end)->_isEnd--;
while (!s.empty())
{
TrieNode** toDelete = s.top();
if ((*toDelete)->_count == 1)//计数器为1了释放内存,否则只更新计数器
{
delete *toDelete;
*toDelete = NULL;
}
else
{
(*toDelete)->_count--;
}
s.pop();
}
}
return false;//trie树中不存在str
}
void release(TrieNode* root)
{
for (int i = 0; i < TrieNode::MAX; ++i)
{
if (root->children[i] != NULL)
{
release(root->children[i]);
}
}
delete root;
}