前言
我们都知道在用搜索引擎进行搜索时,当我们输入部分搜索关键词后,搜索引擎会自动给出一些相似的查询关键词,如在百度输入“Trie”之后,它会自动给出可能与Trie相关的查询。除此之外,我们知道在搜索已经的索引过程中,一个词的词频(TF)的作用很大,那么如何能够快速的统计出一个词在某一篇文档中出现的频率呢?
其实这些问题都可以用Trie树来解决。在计算机科学中,trie,又称前缀树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。trie树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能(引用自维基百科)。下图示意了一个包含26个英文字母的Trie树的结构:
其中节点中标红是节点的标识域,根据具体的应用不同可以为不同的值。如当用于词频统计时,这个域可以是从根节点到该节点的路径所代表的单词出现的次数;当用于查找时,可以是标识从根节点到当前节点所代表的单词是否出现过。下面代码是基于词频统计的一个实现版本:
#define WORDLEN 128 /*单词最大长度*/
#define TREEWIDTH 256 /*字母表大小*/
struct Trie_node
{
int count; /*用于统计当前单词出现的频率*/
struct Trie_node *next[TREEWIDTH]; /*节点指针*/
Trie_node():count(0)
{
memset(next, 0, sizeof(Trie_node*)*TREEWIDTH);
}
};
void insert(Trie_node* root, const char* word)
{
Trie_node* t = root;
while(*word != '\0')
{
if(t->next[*word] == NULL)
{
Trie_node* new_node = new Trie_node;
t->next[*word] = new_node;
}
t = t->next[*word];
word++;
}
t->count++;
}
int search(Trie_node* root, const char* word)
{
int ret = 0;
if(root != NULL)
{
Trie_node* t = root;
while(*word != '\0')
{
if(t->next[*word] == NULL) break;
t = t->next[*word];
word++;
}
if(*word == '\0') ret = t->count;
}
return ret;
}
void deleteTrie(Trie_node *root)
{
if(root == NULL) return;
Trie_node* t = root;
for(int i = 0; i < TREEWIDTH; i++)
{
if(t->next[i] != NULL) deleteTrie(t->next[i]);
}
delete t;
}
static void printword(const char *str, int n)
{
printf("%s\t%d\n", str, n);
}
void printWordF(Trie_node* root)
{
if(root == NULL) return;
static char worddump[WORDLEN + 1];
static int pos = 0;
if(root->count > 0)
{
worddump[pos] = '\0';
printword(worddump, root->count);
}
for(int i = 0; i < WORDLEN; i++)
{
worddump[pos++] = (char)i;
printWordF(root->next[i]);
pos--;
}
}
static inline bool childCount(Trie_node* node)
{
int count = 0;
for(int i = 0; i < TREEWIDTH; i++)
{
if(node->next[i] != NULL) count++;
}
return count;
}
void deleteWord(Trie_node* root, const char *word)
{
stack<Trie_node*> s;
Trie_node* t = root;
//搜索单词word在树中的路径
while(*word != '\0' && t != NULL)
{
t = t->next[*word];
s.push(t);
word++;
}
//根据word在树中的路径,删除属于word独享的一些边
if(*word == '\0')
{
Trie_node* p = NULL;
t->count = 0;
while(!s.empty())
{
word--;
t = s.top();
s.pop();
if(childCount(t) > 0) break;
p = root;
if(!s.empty()) p = s.top();
delete t;
p->next[*word] = NULL;
}
}
}
总结
trie树实际上是一个DFA(确定型自动机),通常可以用于词频统计和搜索提示。在实现时通常用转移矩阵表示。行表示状态,列表示输入字符,(行, 列)位置表示转移状态。这种方式的查询效率很高,但由于稀疏的现象严重,空间利用效率很低。也可以采用压缩的存储方式即链表来表示状态转移,但由于要线性查询,会造成效率低下,以上的实现便是基于链表的。
要了解如何用非链表的形式来实现Trie树,可以参见http://linux.thai.net/~thep/datrie/datrie.html