Trie树的实现

字典树 Trie:高效字符串检索与应用

一、定义:

        Trie,又称字典树,是一种用于快速检索的二十六叉树结构。典型的空间换时间

二、结构图:

          

三、原理:

       Trie把要查找的关键词看作一个字符序列,并根据构成关键词字符的先后顺序检索树结构;

        特别地:和二叉查找树不同,在Trie树中,每个结点上并非存储一个元素。

四、性质:

       0、利用串的公共前缀,节约内存

       1、在trie树上进行检索总是始于根结点

       2、根节点不包含字符,除根节点外的每一个节点都只代表一个字母,这里不是包含。

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

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

五、效率分析

       0、当存储大量字符串时,Trie耗费的空间较少。因为键值并非显式存储的,而是与其他键值共享子串。

       1、查询快。在trie树中查找一个关键字的时间和树中包含的结点数无关,而取决于组成关键字的字符数。对于长度为m的键值,最坏情况下只需花费O(m)的时间(对比:二叉查找树的查找时间和树中的结点数有关O(log2n)。)

       2、如果要查找的关键字可以分解成字符序列且不是很长,利用trie树查找速度优于二叉查找树。

       3、若关键字长度最大是5,则利用trie树,利用5次比较可以从265=11881376个可能的关键字中检索出指定的关键字。而利用二叉查找树至少要进行log2265=23.5次比较。

六、应用:用于字符串的统计与排序,经常被搜索引擎系统用于文本词频统计。

      1、字典树在串的快速检索中的应用。

      给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。在这道题中,我们可以用字典树,先把熟词建一棵树,然后读入文章进行比较,这种方法效率是比较高的。

      2、字典树在“串”排序方面的应用

      给定N个互不相同的仅由一个单词构成的英文名,让你将他们按字典序从小到大输出用字典树进行排序,采用数组的方式创建字典树,这棵树的每个结点的所有儿子很显然地按照其字母大小排序。对这棵树进行先序遍历即可。

      3.、字典树在最长公共前缀问题的应用

      对所有串建立字典树,对于两个串的最长公共前缀的长度即他们所在的结点的公共祖先个数,于是,问题就转化为最近公共祖先问题。

代码

#ifndef _TRIE_
#define _TRIE_

#include <iostream>
#include <fstream>
#include <string>
#include <algorithm>
#include <assert.h>
using namespace std;
const int MaxBranchNum = 26;//如果区分大小写,可以扩展到52

/*定义trie树结点*/
class TrieNode
{
public:
	char* word; //节点表示的单词
	int count;  //单词出现的次数
	TrieNode* nextBranch[MaxBranchNum];//指向26个字符节点的指针
public:
	TrieNode() : word(NULL),count(0)
	{
		memset(nextBranch,NULL,sizeof(TrieNode*) * MaxBranchNum);
	}
};

/*定义类Trie*/
class Trie
{
public:
	Trie();
	~Trie();
	void Insert(const char* str);//插入字符串str
	bool Search(const char* str,int& count);//查找字符串str,并返回出现的次数
 	bool Remove(const char* str);//删除字符串str
	void PrintALL();//打印trie树中所有的结点
	void PrintPre(const char* str);//打印以str为前缀的单词
private:
	TrieNode* pRoot;
private:
	void Destory(TrieNode* pRoot);
	void Print(TrieNode* pRoot);
};

#endif //_TRIE_

Trie::Trie()
{
	pRoot = new TrieNode();//注意字典树的根不存放字符
}

Trie::~Trie()
{
	Destory(pRoot);
}

/*插入一个单词*/
void Trie::Insert(const char* str)
{
	assert(NULL != str);
	int index;
	TrieNode* pLoc = pRoot;
	for (int i = 0;str[i];i++)
	{
		index = str[i] - 'a';//如果区分大小写,可以扩展

		if(index < 0 || index > MaxBranchNum)//不执行插入
		{
			return;
		}

		if (NULL == pLoc->nextBranch[index])//该单词的前缀不存在,要生成该结点
		{
			pLoc->nextBranch[index] = new TrieNode();
		}
		pLoc = pLoc->nextBranch[index];
	}
	if (NULL != pLoc->word)//单词已经出现过
	{
		pLoc->count++;
		return;
	}
	else    //单词没有出现过,应该插入单词
	{
		pLoc->count++;
		pLoc->word = new char[strlen(str) + 1];
		assert(NULL != pLoc->word);
		strcpy(pLoc->word,str);
	}
}

/*查找一个单词,如果存在该单词,则返回其出现次数*/
bool Trie::Search(const char* str,int& count)
{
	assert(str != NULL);
	int i = 0;
	int index = -1;;
	TrieNode* pLoc = pRoot;
	while(pLoc && *str)
	{
		index = *str - 'a';//如果区分大小写,可以扩展

		if(index < 0 || index > MaxBranchNum)//不是一个单词,不执行插入
		{
			return false;
		}

		pLoc = pLoc->nextBranch[index];
		str++;
	}
	if (pLoc && pLoc->word)//条件成立,找到该单词
	{
		count = pLoc->count;
		return true;
	}
	return false;
}

bool Trie::Remove(const char* str)
{
	assert(NULL != str);
	int index = -1;;
	TrieNode* pLoc = pRoot;
	while(pLoc && *str)
	{
		index = *str - 'a';//如果区分大小写,可以扩展

		if(index < 0 || index > MaxBranchNum)//不是一个单词,不执行插入
		{
			return false;
		}

		pLoc = pLoc->nextBranch[index];
		str++;
	}
	if (pLoc && pLoc-> word)//条件成立,找到该单词
	{
		delete[] pLoc->word;
		pLoc->word = NULL;
		return true;
	}
	return false;
}

void Trie::PrintALL()
{
	Print(pRoot);
}

void Trie::PrintPre(const char* str)
{
	assert(str != NULL);
	int i = 0;
	int index = -1;;
	TrieNode* pLoc = pRoot;
	while(pLoc && *str)
	{
		index = *str - 'a';//如果区分大小写,可以扩展

		if(index < 0 || index > MaxBranchNum)//不是一个单词,不执行插入
		{
			return;
		}

		pLoc = pLoc->nextBranch[index];
		str++;
	}
	if (pLoc)//条件成立,找到该单词
	{
		Print(pLoc);
	}
}

/*按照字典顺序输出以pRoot为根的所有的单词*/
void Trie::Print(TrieNode* pRoot)
{
	if (NULL == pRoot)
	{
		return;
	}
	//输出单词
	if (NULL != pRoot->word)
	{
		cout<<pRoot->word<<" "<<pRoot->count<<endl;
	}
	//递归处理分支
	for (int i = 0;i < MaxBranchNum;i++)
	{
		Print(pRoot->nextBranch[i]);
	}
}

/*销毁trie树*/
void Trie::Destory(TrieNode* pRoot)
{
	if (NULL == pRoot)
	{
		return;
	}
	for (int i = 0;i < MaxBranchNum;i++)
	{
		Destory(pRoot->nextBranch[i]);
	}
	//销毁单词占得空间
	if (NULL != pRoot->word)
	{
		delete []pRoot->word;   
		pRoot->word = NULL;
	}
	delete pRoot;//销毁结点
	pRoot = NULL;
}

int main()
{
	Trie t;
	string str;
	int count = -1;
	ifstream in("word.txt");
	//把单词输入字典树
	while(in >> str)
	{
		transform(str.begin(),str.end(),str.begin(),tolower);//大写变小写
		t.Insert(str.c_str());
	}
	//查找
	bool isFind = t.Search("the",count);
	if (isFind)
	{
		cout<<"存在the,出现次数:"<<count<<endl;
	}
	else
	{
		cout<<"不存在the!"<<endl;
	}
	//输出
	t.PrintALL();
	//删除
	bool isDel = t.Remove("the");
	if (isDel)
	{
		cout<<"删除成功!"<<endl;
	}
	else
	{
		cout<<"删除失败!"<<endl;
	}
	//输出以w开头的单词
	t.PrintPre("w");
	cout<<endl;
	system("pause");
}



 

### 3.1 Trie实现原理 Trie(字典)是一种专为字符串处理而设计的多叉结构,其核心原理是将字符串的字符逐层分解,构建出从根节点到叶子节点的一条路径,路径上的字符依次拼接形成完整的字符串。每个节点代表一个字符,路径代表一个字符串前缀,因此可以高效地进行前缀匹配和字符串查找操作。 Trie的三个基本性质包括: - 根节点不包含字符,其余每个节点只包含一个字符。 - 从根节点到某个节点的路径上所有字符连接起来,构成该节点所代表的字符串。 - 每个节点的所有子节点所包含的字符互不相同[^5]。 这种结构使得 Trie 在自动补全、词频统计、拼写检查等场景中具有显著优势,尤其是在大规模字符串集合中进行前缀匹配时效率极高[^4]。 ### 3.2 Trie的插入与查询操作 Trie的两个核心操作是插入和查询。插入操作是指将一个字符串加入 Trie 中,构建路径节点;查询操作则用于判断某个字符串是否存在于中,或者查找具有某一前缀的所有字符串。 插入字符串时,从根节点出发,依次检查字符串中的每个字符是否在当前节点的子节点中存在。若存在则继续向下遍历,否则新建子节点。这一过程持续到字符串最后一个字符插入完成为止。 查询操作的过程与插入类似,逐字符匹配路径。如果路径完整存在,则字符串存在于 Trie 中;若在某一步无法继续匹配,则说明该字符串或其前缀未被 Trie 所包含。 以下是一个 Trie 的 Java 实现示例: ```java class TrieNode { private final TrieNode[] children = new TrieNode[26]; private boolean isEndOfWord; public TrieNode getChild(char ch) { return children[ch - 'a']; } public void setChild(char ch, TrieNode node) { children[ch - 'a'] = node; } public boolean isEndOfWord() { return isEndOfWord; } public void setEndOfWord(boolean endOfWord) { isEndOfWord = endOfWord; } } class Trie { private final TrieNode root = new TrieNode(); public void insert(String word) { TrieNode node = root; for (char ch : word.toCharArray()) { if (node.getChild(ch) == null) { node.setChild(ch, new TrieNode()); } node = node.getChild(ch); } node.setEndOfWord(true); } public boolean search(String word) { TrieNode node = root; for (char ch : word.toCharArray()) { node = node.getChild(ch); if (node == null) { return false; } } return node.isEndOfWord(); } public boolean startsWith(String prefix) { TrieNode node = root; for (char ch : prefix.toCharArray()) { node = node.getChild(ch); if (node == null) { return false; } } return true; } } ``` ### 3.3 Trie的删除与空间效率 Trie的删除操作较为复杂,需考虑是否保留中间节点。若某个字符串的删除导致某一路径上的节点不再被其他字符串共享,则应将这些节点释放以节省空间。这一操作需要回溯路径并判断节点是否可删除,增加了实现的复杂性[^2]。 关于空间效率,Trie存储开销与字符串集合的总长度成正比。在字符串集合中存在大量公共前缀的情况下,Trie可以有效节省空间;但在字符串差异较大的情况下,其空间开销可能较大。因此,实际应用中需权衡空间与时间效率[^1]。 ### 3.4 Trie应用场景 Trie因其高效的前缀匹配能力,广泛应用于搜索引擎的自动补全功能、词频统计、拼写检查等领域。例如,在实现自动补全功能时,用户输入前缀后,系统可在 Trie 中快速查找所有以该前缀开头的字符串,并返回建议列表[^4]。 此外,Trie还可用于实现字符串集合的快速检索,例如在拼写检查中快速判断某个单词是否合法,或在 IP 地址查找中用于最长前缀匹配等场景。 ###
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值