字典树的C++实现 Implement of trie tree

Trie,字典树,又称单词查找树、前缀树,是一种哈希树的变种。应用于字符串的统计与排序,经常被搜索引擎系统用于文本词频统计。

 

性质:

1.根节点不包含字符,除根节点外的每一个节点都只包含一个字符。

2.从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。

3.每个节点的所有子节点包含的字符都不相同。

优点是查询快。对于长度为m的键值,最坏情况下只需花费O(m)的时间;而BST需要O(m log n)的时间。

 

本文用C++实现了字典树的

建立,插入,遍历,查找,删除节点,删除字典树,统计含有指定前缀的单词数等功能。

着重讲述下列功能的实现。

 

插入单词的实现:

1).设置当前节点为根节点,设置当前字符为插入字符串中的首个字符;

2).在当前节点的子节点上搜索当前字符,若存在,则将当前节点设为值为当前字符的子节点;否则新建一个值为当前字符的子节点,并将当前结点设置为新创建的节点。

3).将当前字符设置为串中的下个字符,若当前字符为0,则结束;否则转2.

4).结束时,把最后一个节点(即此时的当前节点)的isEnd设为true,对应下图中的黑色节点。表示该节点是一个单词的结尾。

 《字典树的C++实现 Implement of trie tree》《字典树的C++实现 Implement of trie tree》

查找单词的实现:

过程和插入单词非常类似,这里不再赘述。

 

删除单词的实现:

首先查找待删除字符串,将路径上的节点一个个入栈(本文用递归实现)。

1:若途中某个字符找不到,则不删除。

2:若到达了字符串的末尾的下一个位置,说明字符串中所有字符均找到,把包含字符串最后一个字符的那个节点设为(isEnd = false),表明到该位置不是一个单词的结尾了。

如果保存字符串中最后那个字符的节点是个叶子节点(即childCnt为0),则删除该点。

接下来便一层一层往上返回,判断当前节点是删除还是保留:

若当前节点孩子为0(childCnt为0)并且当前节点不为一个单词的结尾时(isEnd为false),则删除该节点,否则不删除。

 

容易造成的误解是,若当前节点的孩子为0了不就可以删除了吗?其实不然。

使用插入时用的图做说明:

《字典树的C++实现 Implement of trie tree》《字典树的C++实现 Implement of trie tree》

当插入了字符串”abc”,”ad”,”ef”,”e”后,要删除字符串“ef“,遵循上述的步骤,指针会到达黑色的“ef”节点,此时把该节点的isEnd设为false,表示该节点已经不能表示一个单词的结尾了。而且该节点为叶子节点,所以要删除它。然后返回该节点的父节点,即“e”节点,由于它唯一的子节点已经被删除了,所以“e”节点此时已经退化成了叶子节点(childCnt==0),然而此时却不能删除它,因为它是黑色的(是单词“e”的结尾)。所以要判断当前节点是否可以删,必须满足它是叶子节点以及它不是一个单词的结尾这两个条件。

 

统计含有相同前缀的单词个数的实现(!!待改进版):

以属性prefixCnt来统计,经过该节点的单词一共有多少个,这样,在给定一个前缀的时候,按照查询字符串的方法,一步步往下查找,途中出现未找到的字符,则返回0。否则到达包含前缀的最后一个字符的节点,直接返回该节点的prefixCnt的值即可。然而该值是在进行插入单词时更新的,目前的处理只能针对没有重复插入同一个单词的情况。如果要对重复插入同一个单词的情况进行处理,需要使用栈的思维?如果各位有什么好的办法,评论里多多指教。

#include <queue>
#include <iostream>
using namespace std;

const int size = 26;

struct TrieTreeNode
{
	char val;
	bool isEnd;
	int childCnt;
	int prefixCnt;
	TrieTreeNode *child[size];
	TrieTreeNode(char _val)
		:val(_val),isEnd(false),childCnt(0),prefixCnt(0)
	{
		memset(child,NULL,sizeof(child));//not 26!!
	}
};

void Insert(TrieTreeNode *&root, const char *word)
{
	TrieTreeNode *p = root;
	for (int i = 0; i < strlen(word); i++)
	{
		if(p->child[word[i]-'a'] == NULL)
		{
			p->child[word[i]-'a'] = new TrieTreeNode(word[i]);
			p->childCnt++;
		}
		//notice!!this line need to be optimized to handle with duplicated insertion
		p->child[word[i]-'a']->prefixCnt++;

		p = p->child[word[i]-'a'];
	}
	p->isEnd = true;
}

bool Find(TrieTreeNode *root, const char *word)
{
	TrieTreeNode *p = root;
	for (int i = 0; i < strlen(word); i++)
	{
		if (p->child[word[i]-'a'] == NULL)
			return false;
		p = p->child[word[i]-'a'];
	}
	return p->isEnd;
}

void LevelOrderTraverse(TrieTreeNode *root)
{
	if(root == NULL)
		return;
	queue<TrieTreeNode *> Q;
	Q.push(root);
	while (!Q.empty())
	{
		TrieTreeNode *p = Q.front();
		cout << p->val << "(" << p->childCnt << ") ";
		for (int i = 0; i < size; i++)
		{
			if(p->child[i] != NULL)
				Q.push(p->child[i]);
		}
		Q.pop();
	}
	cout << "\n";
}

void PreOrderTraverse(TrieTreeNode *treeNode)
{
	if (treeNode != NULL)
	{
		cout << treeNode->val << "(" << treeNode->childCnt << ") ";
		for (int i = 0; i < size; i++)
		{
			PreOrderTraverse(treeNode->child[i]);
		}
	}
}

void PostOrderTraverse(TrieTreeNode *treeNode)
{
	if (treeNode != NULL)
	{
		for (int i = 0; i < size; i++)
		{
			PostOrderTraverse(treeNode->child[i]);
		}
		cout << treeNode->val << "(" << treeNode->childCnt << ") ";
	}
}

void MakeEmpty(TrieTreeNode *&treeNode)
{
	if (treeNode != NULL)
	{
		for (int i = 0; i < size; i++)
		{
			MakeEmpty(treeNode->child[i]);
		}
		delete treeNode;
	}
	treeNode = NULL;
}

void BuildTrieTree(TrieTreeNode *&root,const char *words[], int n)
{
	for (int i = 0; i < n; i++)
	{
		Insert(root,words[i]);
	}
}

bool Remove(TrieTreeNode *&treeNode, const char *word,int pos, int n) 
{
	if (pos == n)
	{
		treeNode->isEnd = false;//set the node not to be an end
		//if the last node contains the last char is a leaf,return true to delete it
		return treeNode->childCnt == 0;
	}

	//not found, not delete this node
	if (treeNode->child[word[pos]-'a'] == NULL) 
		return false;

	//if true, the child is a leaf, delete the child
	if ( Remove( treeNode->child[word[pos]-'a'], word, pos+1, n))
	{ 
		delete treeNode->child[word[pos]-'a'];
		treeNode->child[word[pos]-'a'] = NULL;

		treeNode->prefixCnt--;
		//if the node becomes a leaf and is not an end,return true to delete it
		if (--treeNode->childCnt == 0 && treeNode->isEnd == false)
			return true;
	}

	//other not delete
	return false;
}

//Count the number of words which contain the specific prefix
int CountWordsWithPrefix(TrieTreeNode *root, const char *prefix)
{
	TrieTreeNode *p = root;
	for (int i = 0; i < strlen(prefix); i++)
	{
		if(p->child[prefix[i]-'a'] == NULL)
			return 0;
		
		p = p->child[prefix[i]-'a'];
	}
	return p->prefixCnt;
}

int main()
{
	TrieTreeNode *root = new TrieTreeNode('\0');
	//const char *words[] = {"b","abc","abd","bcd","abcd","efg","hii"}; //test insert
	//cout << sizeof(words) << "\n";//(4*7=28)
	//cout << sizeof(words[0]) << "\n";//4(is a pointer)

	const char *words[] = {"abc","ad","ef"};//test remove
	BuildTrieTree(root,words,sizeof(words)/sizeof(words[0]));
	LevelOrderTraverse(root);
	PreOrderTraverse(root);
	cout << '\n';
	PostOrderTraverse(root);
	cout << "\n";

	if (Find(root,"ef"))
		cout << "ef found" << endl;
	else cout << "ef not found" <<endl;

	Insert(root,"e");
	//after this insertion.the node 'e' becomes a end but it's not a leaf,
	//so it can not be deleted unless its leaf(leaves) is deleted
	LevelOrderTraverse(root);

    Remove(root,"ef",0,strlen("ef"));
	LevelOrderTraverse(root);

	Remove(root,"e",0,strlen("e"));
	LevelOrderTraverse(root);

	cout << CountWordsWithPrefix(root,"a")<<endl;
	Remove(root,"ad",0,strlen("ad"));
	cout << CountWordsWithPrefix(root,"a")<<endl;

	MakeEmpty(root);
	
	return 0;
}

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