Trie树解析

前言

        我们都知道在用搜索引擎进行搜索时,当我们输入部分搜索关键词后,搜索引擎会自动给出一些相似的查询关键词,如在百度输入“Trie”之后,它会自动给出可能与Trie相关的查询。除此之外,我们知道在搜索已经的索引过程中,一个词的词频(TF)的作用很大,那么如何能够快速的统计出一个词在某一篇文档中出现的频率呢?

                     《Trie树解析》

        其实这些问题都可以用Trie树来解决。在计算机科学中,trie,又称前缀树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。trie树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能(引用自维基百科)。下图示意了一个包含26个英文字母的Trie树的结构:

                                                      《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


    原文作者:Trie树
    原文地址: https://blog.csdn.net/acaiwlj/article/details/9388025
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注