Trie字典树详解

news2024/11/27 20:37:36

字典树

    • 📖1. 什么是Trie树
    • 📖2. Trie树的一些应用场景
    • 📖3. Trie树的优缺点
    • 📖4. Trie树的节点怎样定义
    • 📖5. 代码实现
    • 📖6. 字典树的优化

📖1. 什么是Trie树

Trie树,又叫字典树,前缀树(Prefix Tree),单词查找树,是一种多叉树的结构.

image-20221122001519100

上图就是一颗Trie树,表示了关键字集合{"pool", "prize", "prepare", "preview", "produce", "progress"}.

字典树的基本性质如下:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  2. 从根节点到某一节点,路径上的字符连接起来,为该节点对应的字符串
  3. 每个节点的所有子节点包含的字符都不相同.

算法核心:利用字符串的公共前缀来减少查询时间,最大限度的减少无畏字符串的比较.

使用范围:

  1. 单词检索
  2. 统计和排序字符串
  3. 字符串前缀搜索

我们来看一个场景:

image-20221122002220430

当我们在浏览器的搜索框中打出一个字符串的前缀时,它便实时的显示出了以这个输入为前缀的一些字符串,也就是说,它帮我们搜索到了以这个输入为前缀的所有字符串,并且显示出了搜索频率较高的一些,这就是字典树的一个应用场景:单词自动补齐.

📖2. Trie树的一些应用场景

除了自动补齐的例子外,Trie树还有一些其他的应用场景:

  1. 串的快速检索

    给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词.

  2. 单词自动完成

    编辑代码时,输入字符,自动提示可能的关键字,变量或函数等信息.

  3. 最长公共前缀

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

  4. 串排序方面的应用

    给定N个互不相同的仅由一个单词构成的英文名,将他们按字典序从小到大输出. 用字典树进行排序,这棵树的每个节点的所有子节点很明显的按照其字母大小排序,对这棵树进行前序遍历即可.

📖3. Trie树的优缺点

Trie树的核心思想是空间换时间,利用字符串的公共前缀来减少无畏字符串的比较以达到提高查询效率的目的.

优点:插入和查询的效率高,都为O(m),其中m是待插入/查询的字符串的长度.

关于查询,可能有人会说,哈希表的时间复杂度为O(1),岂不是更快?

  1. 哈希表的效率通常取决于哈希函数的好坏,若一个坏的哈希函数可能会导致发生许多冲突,将具有相似前缀的字符串映射到同一个哈希桶中,在进行这个哈希桶中进行查找时,就会导致这些相同前缀的重复比较,效率并不一定比Trie树高.
  2. Trie树中的不同关键字不会产生哈希冲突
  3. Trie树可以对关键字按字典序排序

缺点当所有关键字都不具有相同或类似的前缀,空间消耗过大.

📖4. Trie树的节点怎样定义

对于Trie树,它是一个多叉树的结构,所有我们可以在节点中定义map来存储孩子节点字符数据和节点指针的对应关系.

在查找的过程中,我们怎样标定字符串的结尾呢?

image-20221122005701772

所以节点的定义:

struct TrieNode
{
    //节点中存储的字符数据
    char ch_;
    //单词的末尾字符存储单词的数量
    int freqs_;
    //存储孩子节点字符数据和节点指针的对应关系
    std::map<char, TrieNode*> nodeMap_;
};

📖5. 代码实现

#include<iostream>
#include<map>
#include<string>
#include<vector>
#include<queue>

using namespace std;

class TrieTree
{
public:
	TrieTree()
	{
		root_ = new TrieNode('\0', 0);
	}
public:
	//添加单词
	void add(const string& word)
	{
		TrieNode* cur = root_;

		for (int i = 0; i < word.size(); ++i)
		{
			auto childIt = cur->nodeMap_.find(word[i]);
			if (childIt == cur->nodeMap_.end())
			{
				//相应的字符的节点没有,创建它
				TrieNode* child = new TrieNode(word[i], 0);
				cur->nodeMap_.emplace(word[i], child);
				cur = child;
			}
			else
			{
				//相应的字符节点已经存在,移动cur指向对应的节点
				cur = childIt->second;
			}
		}

		//cur指向了word单词的最后一个节点
		cur->freqs_++;
	}

	//查询单词
	int query(const string& word)
	{
		TrieNode* cur = root_;
		for (int i = 0; i < word.size(); ++i)
		{
			auto childIt = cur->nodeMap_.find(word[i]);
			if (childIt == cur->nodeMap_.end())
			{
				return 0;
			}

			//移动cur指向下一个单词的字符节点上
			cur = childIt->second;
		}

		//最终指向了这个单词的最后一个字符
		return cur->freqs_;
	}

	//删除单词
	void remove(const string& word)
	{
		TrieNode* cur = root_;
		TrieNode* del = root_;  //从哪个节点开始删除
		char delch = word[0];

		for (int i = 0; i < word.size(); ++i)
		{
			auto childIt = cur->nodeMap_.find(word[i]);
			if (childIt == cur->nodeMap_.end())
				return;

			//pool po情况二和情况三
			if (cur->freqs_ > 0 || cur->nodeMap_.size() > 1)
			{
				del = cur;
				delch = word[i];
			}

			//cur移动到子节点
			cur = childIt->second;
		}

		//cur指向了末尾节点
		if (cur->nodeMap_.empty())
		{
			//开始删除
			TrieNode* child = del->nodeMap_[delch];
			del->nodeMap_.erase(delch);

			//释放相应的节点内存
			queue<TrieNode*> que;
			que.push(child);

			while (!que.empty())
			{
				TrieNode* front = que.front();
				que.pop();

				//把当前节点的孩子全部入队列
				for (auto& pair : front->nodeMap_)
				{
					que.push(pair.second);
				}

				//释放当前节点资源
				delete front;
			}

		}
		else
		{
			//情况1
			//当前单词末尾字符后面还有字符节点,不做任何节点删除操作
			cur->freqs_ = 0;
		}

	}

	//前序遍历字典树
	void preOrder()
	{
		string word;
		vector<string> wordList;
		preOrder(root_, word, wordList);
		for (auto word : wordList)
		{
			cout << word << endl;
		}
		cout << endl;
	}

	//串的前缀搜索
	vector<string> queryPrefix(const string& prefix)
	{
		TrieNode* cur = root_;
		for (int i = 0; i < prefix.size(); ++i)
		{
			auto childIt = cur->nodeMap_.find(prefix[i]);
			if (childIt == cur->nodeMap_.end())
			{
				return {};
			}

			cur = childIt->second;
		}
		
		//cur指向了前缀的最后一个字符节点了
		vector<string> wordList;
		preOrder(cur, prefix.substr(0, prefix.size() - 1), wordList);
		return wordList;
	}

private:
	struct TrieNode
	{
		TrieNode(char ch, int freqs)
			: ch_(ch)
			, freqs_(freqs)
		{}
		//节点中存储的字符数据
		char ch_;
		//单词的末尾字符存储单词的数量
		int freqs_;
		//存储孩子节点字符数据和节点指针的对应关系
		std::map<char, TrieNode*> nodeMap_;
	};

private:
	void preOrder(TrieNode* cur, string word, vector<string>& wordList)
	{
		//前序遍历 根 左 右
		if (cur != root_)
		{
			word.push_back(cur->ch_);
			if (cur->freqs_ > 0)
			{
				wordList.push_back(word);
			}
		}

		//递归处理子节点
		for (auto pair : cur->nodeMap_)
		{
			preOrder(pair.second, word, wordList);
		}
	}

private:
	TrieNode* root_; //指向树的根结点
};
//功能测试:
#include "TrieTree.h"

int main()
{
	TrieTree trie;
	trie.add("hello");
	trie.add("hello");
	trie.add("helloo");
	trie.add("hel");
	trie.add("hel");
	trie.add("hel");
	trie.add("china");
	trie.add("ch");
	trie.add("ch");
	trie.add("heword");
	trie.add("hellw");

	cout << "数量统计: " << endl;
	cout << trie.query("hello") << endl;
	cout << trie.query("helloo") << endl;
	cout << trie.query("hel") << endl;
	cout << trie.query("china") << endl;
	cout << trie.query("ch") << endl;

	cout << "=====================" << endl;
	cout << "前序遍历: " << endl;
	trie.preOrder();
	cout << "=====================" << endl;
	vector<string> words = trie.queryPrefix("he");
	cout << "前缀串搜索: " << endl;
	for (auto word : words)
	{
		cout << word << endl;
	}
	cout << endl;

	trie.remove("hellw");
	cout << "=====================" << endl;
	cout << "删除测试: " << endl;
	trie.preOrder();

	return 0;
}

📖6. 字典树的优化

上面我们提到过,字典树有一个缺点:占用内存空间过大.

所以,我们可以将字典树进行优化

字符节点后面没有其他单词,可以把字符节点压缩到一个节点中进行存储.

image-20221122160851946

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/27017.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux性能中常用压测工具

stress工具 stress是Linux的一个压力测试工具&#xff0c;可以对CPU、Memory、IO、磁盘进行压力测试。 安装: sudo yum install stress 命令的使用: -c, --cpu N&#xff1a;产生N个进程&#xff0c;每个进程都循环调用sqrt函数产生CPU压力。 -i, --io N&#xff1a;产生N个进…

Java搭建实战springboot基于若依项目工时统计成本核算管理源码

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 本期给大家带来一套基于若依开发的springboot项目工时统计成本核算管理源码&#xff0c;该系统是前后端分离的架构&#xff0c;前端使用Vue2&#xff0c;后端使用SpringBoot2。 技术架构 技术框架&#xff1a;Sp…

计算机毕设推荐基于微信小程序的自来水收费系统

&#x1f496;&#x1f525;作者主页&#xff1a;计算机毕设老哥&#x1f525; &#x1f496; 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; Java实战项目专栏 Python实…

帝国CMS后台登录显示空白解决方法汇总

升级PHP版本由原来5.X切换使用PHP7.*运行环境时会出现登录后台空白的情况,以下两种方法可设置支持PHP7.* 帝国CMS后台登录显示空白解决方法操作步骤: 1、全新安装帝国CMS时: 只需安装时“MYSQL接口类型”选择“mysqli”即可支持PHP7系列。系统会自动识别。 2、已经运行的帝国…

送给python初学者的福利!掌握了这些知识,你将超过80%的小白

前言 Python要学多久才能学会&#xff1f;我没有接触过编程怎么办&#xff1f;这是每一个初学者心中的疑问。 小编曾在网上看到一个帖子&#xff1a; 有一名网友是学金融的&#xff0c;他去上海找工作&#xff0c;看到好多公司都要有 Python 开发经验的&#xff0c;而且工资…

UE5笔记【八】导入FBX网格和和材质到UE5

资源网站&#xff1a;TurboSquid。进入&#xff0c;然后搜索Crate。筛选条件&#xff1a;Free。然后找到这个木箱子。 注册下载时&#xff0c;看到好多3D软件。 点击fbx.zip下载。然后解压到相应目录下。 接下来就是如何导入到UE5中&#xff0c;使用这个素材了。 第一种方式&…

MIT6.824 2022 Raft

MIT6.824 2022 RaftRaftleader electionlogpersistencelog compaction整体测试Raft leader election 不论是访问还是修改Raft可变类成员&#xff0c;都需要加锁 rf.mu.Lock() if rf.state ! Leader {rf.mu.Unlock()return } args : AppendEntriesArgs{Term: rf.currentTerm,…

某网站X-Signature签名破解

网站地址: aHR0cHM6Ly9mc2UuYWdpbGVzdHVkaW8uY24= (b64解密) 先全局搜索X-Signature,跳转到app.xxx.js里,找到X-Signature,发现是对请求参数做了_操作,向上_为d 向上找d, d为一个函数, 跟到这里打印i值(“_platform=web,_ts=1669108721743,_versioin=0.2.5,keyword=…

华为服务体系:ITR流程体系详解

如果开发出了好产品&#xff0c;同时也高效率地将产品交付到客户手中。 但客户最终还是选择投诉&#xff0c;而且复购率低&#xff0c;原因是什么呢&#xff1f; 这就跟ITR流程有关了&#xff0c;在ITR不顺畅的时候&#xff0c;企业可能会面临如下挑战&#xff1a; 客户总是…

Serverless 架构下的 AI 应用开发

作者&#xff1a;阿里云云原生 本篇内容连载自《Serverless 架构下的 AI 应用开发&#xff1a;入门、实战与性能优化》。 Serverless 架构与 CI/CD 工具的结合 CI/CD 是一种通过在应用开发阶段引入自动化流程以频繁向客户交付应用的方法。 如图所示&#xff0c;CI/CD 的核心概…

Vue3中的computed和watch属性

文章目录1. computed计算属性2. watch侦听器属性2.1 watchEffect2.2 watch1. computed计算属性 简写写法&#xff08;只实现了 get &#xff09;&#xff1a; <template><div><ul><li v-for"item of carts" :key"item.id">{{ it…

精华推荐 | 【深入浅出RocketMQ原理及实战】「底层原理挖掘系列」透彻剖析贯穿RocketMQ的存储系统的实现原理和持久化机制

RocketMQ的发展历史 RocketMQ是一个统一消息引擎、轻量级数据处理平台。RocketMQ是一款阿里巴巴开源的消息中间件。 2016 年 11 月 28 日&#xff0c;阿里巴巴向 广西党性培训 Apache 软件基金会捐赠RocketMQ&#xff0c;成为 Apache 孵化项目。 2017 年 9 月 25 日&#xff0…

webpack安装与基础

概念 webpack是一个前端打包工具用它来处理现代前端错综复杂的依赖关系&#xff08;A插件需要B插件B插件有D插件 F插件需要A插件&#xff09;生成浏览器可以识别静态资源Vue 前期脚手架就是用webpack制作&#xff08;Vue 开始推荐vite构建工具&#xff08;更快&#xff09;&am…

[UE][UE5]Gameplay框架,Actor,pawn,playerController(玩家控制器),Character(角色)之间的关系

[UE][UE5]Gameplay框架,actor,pawn,playerController,Character之间的关系Actor,pawn,playerController(玩家控制器),Character(角色)之间的关系Actor&#xff1a;pawn&#xff1a;character&#xff1a;控制器&#xff08;Controller&#xff09;&#xff1a;playerController…

sqlServer如何实现分页查询

sqlServer的分页查询和mysql语句不一样&#xff0c;有三种实现方式。分别是&#xff1a;offset /fetch next、利用max&#xff08;主键&#xff09;、利用row_number关键字 一、offset /fetch next关键字 2012版本及以上才有&#xff0c;SQL server公司升级后推出的新方法。 …

Shiro前后端分离流程

1.自定义filter 拦截所有携带token的请求&#xff0c; 调用自定义realm&#xff0c;判断token是否正确&#xff0c;不正确realm抛出异常&#xff0c;在filter中被捕获&#xff0c;重定向至token不正确界面 重写了三个方法&#xff1a; 1》isAccessAllowed&#xff1a;如果带…

有一个项目管理软件,名字叫8Manage PM!

优秀的软件工具在项目管理中起到极为重要的作用。8Manage PM项目管理软件由高亚科技自主研发&#xff0c;为项目工作提供项目功能、业务功能、服务功能和工具&#xff0c;有力推动项目成功。 8Manage软件项目功能包括完整性管理、需求管理、计划和执行、资源管理、工作量&…

锐捷BGP基础配置

目录 ​编辑 配置IBGP邻居 配置EBGP邻居 BGP其它配置 配置IBGP邻居 R2、R3、R4底层IGP互通&#xff0c;此处IGP互通配置不做介绍 R2与R4通过Loop0建立IBGP邻居&#xff0c;R3与R4通过Loop0建立IBGP邻居 R4充当反射器&#xff0c;R2和R3作为客户端&#xff08;通过反射可以将…

Vue中设置背景图片和透明度

如果文章对你有帮助欢迎【关注❤️❤️❤️点赞&#x1f44d;&#x1f44d;&#x1f44d;收藏⭐⭐⭐】一键三连&#xff01;一起努力&#xff01; 今天来为我自己的项目设置一个好看的登录页面之前是这样的&#xff1a; 乍一看感觉还行&#xff0c;越看越难受&#xff0c;弄一…

Nodejs http模块常用方法

视频链接&#xff1a;黑马程序员Node.js全套入门教程 文章目录http模块1 什么是http模块2 进一步理解http的作用3 服务器相关的概念3.1 IP地址3.2 域名和域名服务器3.3 端口号4 创建简单的web服务器1 步骤2 代码实现3 req请求对象4 res响应对象5 解决中文乱码问题5 简单路由效果…