【数据结构与算法】前缀树的实现

news2024/11/28 16:53:54

🌠作者:@阿亮joy.
🎆专栏:《数据结构与算法要啸着学》
🎇座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述


目录

    • 👉前缀树的实现👈
      • 什么是前缀树
      • 节点的定义
      • 构造函数
      • 插入字符串
      • 查找字符串和前缀
      • 析构函数
      • 删除字符串
      • 打印前缀树
      • 完整代码
      • OJ题:实现前缀树
    • 👉总结👈

👉前缀树的实现👈

什么是前缀树

Trie(发音类似 “try”),被称为前缀树或字典树,是一种树形的数据结构,可用于高效地存储和检索字符串数据集中的键。这个数据结构有相当多的应用情景,例如自动补完和拼写检查。下图就是经典的前缀树,我们接下来要实现的前缀树的节点存储的数据比较丰富,以达到特定字符串在树中出现几次等类似的功能。

在这里插入图片描述

节点的定义

// 前缀树节点的定义
// 假设字符都是小写字母
struct TrieNode
{
	int pass = 0;	// 有几个字符串经过该节点(前缀包含这个字符的字符串数量)
	int end = 0;	// 以该节点为结尾的字符串的数量,如果不允许字符串重复插入,可以改成bool
	// next[0] == nullptr 表示没有走向'a'的路
	// next[0] != nullptr 表示有走向'a'的路
	// ...
	// next[25] != nullptr 表示有走向'z'的路
	TrieNode* next[26] = { nullptr };	// 26个空位,准备挂下一个节点'a'-'z',没有挂节点时为nullptr
	// 如果字符种类个数比较多,可以将数组换成哈希表或者set
};

构造函数

前缀树是用哨兵位头节点来管理整棵前缀树的,所以其构造函数需要 new 上一个哨兵位头节点。

class Trie
{
	typedef TrieNode Node;
public:
	Trie()
	{
		_root = new Node();
	}
private:
	Node* _root = nullptr;	// 哨兵位头节点,可以用来求前缀树中字符串的数量,也可以求空串的数量
};

注:哨兵位头节点的 pass 值可以表示前缀树含有的字符串数量,end 值可以表示前缀树含有空串的数量。因为任何字符串都会以空串作为前缀,都会经过哨兵位头节点。

插入字符串

我们从哨兵位头节点开始,插入字符串。对于当前字符对应的子节点,有以下两种情况:

  • 子节点存在:沿着指针移动到子节点,继续处理下一个字符。
  • 子节点不存在:创建一个新的子节点,记录在 next 指针数组的对应的位置上,然后沿着指针移动到子节点,继续处理下一个字符。
  • 插入字符串的同时,还需要更新沿途节点的 pass 值。

插入字符串图解:

在这里插入图片描述

class Trie
{
public:
	void Insert(const string& str)
	{
		Node* cur = _root;
		++cur->pass;	// 任何一个字符串都需要经过哨兵位头节点

		for (size_t i = 0; i < str.size(); ++i)
		{
			size_t index = str[i] - 'a';
			// 如果之前没有字符串经过该节点,则需要建出新节点
			if (cur->next[index] == nullptr)
			{
				cur->next[index] = new Node();
			}
			cur = cur->next[index];
			++cur->pass;
		}
		// cur指向字符串的最后一个节点,++cur->end表示多了一个字符串以该节点结尾
		++cur->end;
	}
}

如果不需要插入重复字符串,可以将函数的返回值改成 bool 类型。

查找字符串和前缀

class Trie
{
public:
	// 查找前缀树中有多少个要查找的字符串
	size_t Search(const string& str) const
	{
		Node* cur = _root;
		for (auto ch : str)
		{
			// 找的过程发现没路了,说明树中不存在要查找的字符串
			if (cur->next[ch - 'a'] == nullptr)
			{
				return 0;
			}
			cur = cur->next[ch - 'a'];
		}
		// cur是str最后一个字符,cur->end表示树中有多少个str
		return cur->end;
	}
	
	// 查找树中有多少个字符串以前缀prefix为前缀
	size_t StartsWith(const std::string& prefix) const
	{
		Node* cur = _root;
		for (auto ch : prefix)
		{
			// 找的过程中发现没有路,则说明没有字符串以prefix为前缀
			if (cur->next[ch - 'a'] == nullptr)
			{
				return 0;
			}
			cur = cur->next[ch - 'a'];
		}
		// cur->pass表示有多少个字符串以prefix为前缀
		return cur->pass;
	}
}

注:查找的过程和插入的过程非常的相似,只是查找时发现没有路,就直接返回 0,表示树中没有该字符串或者树中的字符串不以 prefix 为前缀。注:如果树中有要查找的字符串 str,则 cur->end 表示树中有多少个 str;如果树有字符串以 prefix 为前缀,则 cur->pass 表示多少个字符串以 prefix 为前缀。

析构函数

class Trie
{
	typedef TrieNode Node;
public:
	~Trie()
	{
		Destroy(_root);
	}
private:
	void Destroy(Node* root)
	{
		// 先销毁孩子节点,才能够销毁自己
		for (int i = 0; i < 26; ++i)
		{
			// root->next[i]不为空,则说明有节点,需要递归释放节点
			if (root->next[i] != nullptr)
			{
				Destroy(root->next[i]);
			}
		}
		delete root;
	}
}

前缀树析构时,需要先释放孩子节点,才能够释放哨兵位头节点。而孩子节点有可能会有孩子节点,所以我们可以采用递归去释放节点。

删除字符串

class Trie
{
	typedef TrieNode Node;
public:
	// 从树中删除字符串str,注:如果有多个str,只会删除一次
	void Erase(const string& str)
	{
		// 树中没有str,无法删除
		if (Search(str) == 0)
			return;

		Node* cur = _root;
		--cur->pass;

		for (size_t i = 0; i < str.size(); ++i)
		{
			size_t index = str[i] - 'a';
			// 如果发现str是唯一经过该节点的字符串
			// 那么就需要递归去释放当前节点及后续路径的节点
			if (--cur->next[index]->pass == 0)
			{
				Destroy(cur->next[index]);	// 递归释放节点
				cur->next[index] = nullptr;	// next需要置为nullptr
				return;
			}
			cur = cur->next[index];
		}
		// 如果字符串的所有字符都删除了一遍,还有该路径,那么最后要
		// --cur->end,表明树中str的个数减少了一个
		--cur->end;
	}
}

删除字符串时,需要看树中是否有需要删除的字符串。如果没有,直接 return 即可。如果有,才进行删除。进行删除时,如果发现 str 是唯一经过该节点的字符串,那么就需要递归去释放当前节点及后续路径的节点。

打印前缀树

class Trie
{
	typedef TrieNode Node;
public:
	void Print() const
	{
		cout << "根节点:[" << "pass: " << _root->pass << " end: " << _root->end << "]" << endl;
		_Print(_root);
	}
private:
	void _Print(Node* root) const
	{
		if (root == nullptr)
			return;
		for (int i = 0; i < 26; ++i)
		{
			if (root->next[i] == nullptr)
				continue;
			else
			{
				cout << "节点" << (char)('a' + i) << ":[pass: " << root->next[i]->pass << " end: " << root->next[i]->end << "]" << endl;
				_Print(root->next[i]);
			}
		}
	}
}

完整代码

#pragma once

#include <vector>
#include <string>
#include <iostream>
using namespace std;

// 前缀树节点的定义
// 假设字符都是小写字母
struct TrieNode
{
	int pass = 0;	// 有几个字符串经过该节点(前缀包含这个字符的字符串数量)
	int end = 0;	// 以该节点为结尾的字符串的数量,如果不允许重复插入,可以改成bool
	// next[0] == nullptr 表示没有走向'a'的路
	// next[0] != nullptr 表示有走向'a'的路
	// ...
	// next[25] != nullptr 表示有走向'z'的路
	TrieNode* next[26] = { nullptr };	// 26个空位,准备挂下一个节点'a'-'z',没有挂节点时为nullptr
	// 如果字符种类个数比较多,可以将数组换成哈希表或者set
};

class Trie
{
	typedef TrieNode Node;
public:
	Trie()
	{
		_root = new Node();
	}

	~Trie()
	{
		Destroy(_root);
	}

	// 查找前缀树中有多少个要查找的字符串
	size_t Search(const string& str) const
	{
		Node* cur = _root;
		for (auto ch : str)
		{
			// 找的过程发现没路了,说明树中不存在要查找的字符串
			if (cur->next[ch - 'a'] == nullptr)
			{
				return 0;
			}
			cur = cur->next[ch - 'a'];
		}
		// cur是str最后一个字符,cur->end表示树中有多少个str
		return cur->end;
	}

	// 查找树中有多少个字符串以前缀prefix为前缀
	size_t StartsWith(const std::string& prefix) const
	{
		Node* cur = _root;
		for (auto ch : prefix)
		{
			// 找的过程中发现没有路,则说明没有字符串以prefix为前缀
			if (cur->next[ch - 'a'] == nullptr)
			{
				return 0;
			}
			cur = cur->next[ch - 'a'];
		}
		// cur->pass表示有多少个字符串以prefix为前缀
		return cur->pass;
	}

	// 插入字符串
	void Insert(const string& str)
	{
		Node* cur = _root;
		++cur->pass;	// 任何一个字符串都需要经过哨兵位头节点

		for (size_t i = 0; i < str.size(); ++i)
		{
			size_t index = str[i] - 'a';
			// 如果之前没有字符串经过该节点,则需要建出新节点
			if (cur->next[index] == nullptr)
			{
				cur->next[index] = new Node();
			}
			cur = cur->next[index];
			++cur->pass;
		}
		// cur指向字符串的最后一个节点,++cur->end表示多了一个字符串以该节点结尾
		++cur->end;
	}

	// 从树中删除字符串str,注:如果有多个str,只会删除一次
	void Erase(const string& str)
	{
		// 树中没有str,无法删除
		if (Search(str) == 0)
			return;

		Node* cur = _root;
		--cur->pass;

		for (size_t i = 0; i < str.size(); ++i)
		{
			size_t index = str[i] - 'a';
			// 如果发现str是唯一经过该节点的字符串
			// 那么就需要递归去释放当前节点及后续路径的节点
			if (--cur->next[index]->pass == 0)
			{
				Destroy(cur->next[index]);	// 递归释放节点
				cur->next[index] = nullptr;	// next需要置为nullptr
				return;
			}
			cur = cur->next[index];
		}
		// 如果字符串的所有字符都删除了一遍,还有该路径,那么最后要
		// --cur->end,表明树中str的个数减少了一个
		--cur->end;
	}

	void Print() const
	{
		cout << "根节点:[" << "pass: " << _root->pass << " end: " << _root->end << "]" << endl;
		_Print(_root);
	}

private:
	void Destroy(Node* root)
	{
		// 先销毁孩子节点,才能够销毁自己
		for (int i = 0; i < 26; ++i)
		{
			if (root->next[i] != nullptr)
			{
				Destroy(root->next[i]);
			}
		}
		delete root;
	}

	void _Print(Node* root) const
	{
		if (root == nullptr)
			return;
		for (int i = 0; i < 26; ++i)
		{
			if (root->next[i] == nullptr)
				continue;
			else
			{
				cout << "节点" << (char)('a' + i) << ":[pass: " << root->next[i]->pass << " end: " << root->next[i]->end << "]" << endl;
				_Print(root->next[i]);
			}
		}
	}

private:
	Node* _root = nullptr;	// 哨兵位头节点,可以用来求前缀树中字符串的数量,也可以求空串的数量
};

前缀树的测试

void TrieTest()
{
	Trie t;
	vector<string> v = { "abc","abd", "abe", "abe", "" ,"a" , "bc", "bd", "be" };
	for (string& str : v)
	{
		t.Insert(str);
	}
	// 前缀树的打印
	t.Print();
	cout << "----------------------" << endl;

	// 输出空串的数量
	cout << "空串的数量: " << t.Search("") << endl;
	// 任意字符串均以空串为前缀/树中字符串的数量
	cout << "树中字符串的数量: " << t.StartsWith("") << endl;
	// 以"ab"为前缀的字符串个数
	cout << "以ab为前缀的字符串个数: " << t.StartsWith("ab") << endl;

	cout << "----------------------" << endl;
	// 测试删除
	for (string& str : v)
	{
		t.Erase(str);
	}
	t.Print();
}

在这里插入图片描述

OJ题:实现前缀树

这里是引用

LeetCode 上的实现前缀树是比我们实现的前缀树是要难度低的,所以只需要将上面的代码拷贝过去,再将函数名和函数的返回值修改成题目要求的样子就可以通过了。

在这里插入图片描述

👉总结👈

本篇博客主要讲解了什么是前缀树以及前缀树的实现等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

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

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

相关文章

54 循环神经网络 RNN【动手学深度学习v2】

54 循环神经网络 RNN【动手学深度学习v2】 深度学习学习笔记 学习视频&#xff1a;https://www.bilibili.com/video/BV1D64y1z7CA/?spm_id_from333.880.my_history.page.click&vd_source75dce036dc8244310435eaf03de4e330 对序列化数据集的训练网络&#xff0c;通常称为RN…

第三章 Opencv图像像素操作

目录1.像素1-1.确定像素位置1-2.获取指定像素的像素值1-3.修改像素的BGR值2.用numpy模块操作像素2-1.创建图像1.创建黑白图像2.创建彩色图像3.创建随机图像2-2.拼接图像1.水平拼接hstack()方法2.垂直拼接vstack()方法1.像素 1.像素是构成数字图像的最小单位。每一幅图像都是由M…

【第29天】SQL进阶-查询优化- performance_schema系列实战四:查看最近的SQL执行信息(SQL 小虚竹)

回城传送–》《32天SQL筑基》 文章目录零、前言一、 查看最近的top sql1.1 数据准备&#xff08;如果已有数据可跳过此操作&#xff09;1.2 查询events_statements_summary_by_digest表二、查看最近执行失败的SQL2.1 开启第一个会话&#xff0c;执行错误sql2.2 开启第二个会话&…

pytest当中pytest.ini使用

目录 一、作用 二、存放位置 三、功能&#xff08;只列了简单的&#xff09; 1、 addopts 2、更改测试用例收集规则 四、运行就减少了命令了 前言&#xff1a;pytest配置文件可以改变pytest的运行方式&#xff0c;它是一个固定的文件pytest.ini文件。 一、作用 pytest.in…

Ceph分部署存储知识总结

Ceph 一.deploy-ceph部署 投入使用ceph前&#xff0c;要知道一个很现实得问题&#xff0c;ceph对低版本内核得客户端使用非常不友好&#xff0c;低内核是指小于等于3.10.0-862&#xff0c;默认的centos7.5及以下的系统都是小于此类内核&#xff0c;无法正常使用ceph的文件存储…

内网渗透(十一)之内网信息收集-内网IP扫描和发现

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…

用YOLOv8推荐的Roboflow工具来训练自己的数据集

YOLOv8是Ultralytics公司开发的YOLO目标检测和图像分割模型的最新版本&#xff0c;相较于之前的版本&#xff0c;YOLOv8可以更快速有效地识别和定位图像中的物体&#xff0c;以及更准确地分类它们。 作为一种深度学习技术&#xff0c;YOLOv8需要大量的训练数据来实现最佳性能。…

如何旋转YUV图片数据且使用Qt显示

前言 提一下这篇文章的需求&#xff1a;将USB相机获取到的YUV数据进行旋转&#xff0c;然后转为QImage进行显示。原本程序中是有旋转的代码&#xff0c;但不知道为什么&#xff0c;旋转出来的图片会花屏。关于花屏的问题&#xff0c;后面会稍微阐述一下。所以&#xff0c;经过…

[多线程进阶] 常见锁策略

专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录: 1. 常见的锁策略 1.1 乐观锁 vs 悲观锁 1.2 读写…

bootstrap 框架

文章目录bootstrap必须使用 HTML5 文档类型排版和链接默认栅格系统带有基本栅格的 HTML 代码媒体类型媒体类型逻辑运算符 用来做条件判断页面布局&#xff1a; 引入 css&#xff08;bootstrap.min.css&#xff09; 类名03-面包屑导航警告框、徽章、面包屑导航、按钮、按钮组卡…

css行内块元素垂直居中

css行内块元素垂直居中 div里边有个img标签&#xff0c;要想让img垂直居中&#xff0c;需要 给父盒子设置line-heightheightimg设置vertical-align:middle <div style"background-color: red; height: 150px;line-height: 150px;"><img src"images/…

Unity开发环境配置

Unity本体安装 1.首先下载安装unityhub,中文管网https://unity.cn/ 2.登录unityhub&#xff0c;选择你想要的版本安装 选择后按照提示选择个人免费试用的license,然后等待unity本体下载安装即可。 VSCode安装和配置 1.去官网https://code.visualstudio.com/下载vscode 2.u…

微信小程序 Springboot ssm房屋租赁系统uniapp设计与实现

房屋租赁系统用户和户主是基于微信端&#xff0c;管理员是基于网页端&#xff0c;系统采用java编程语言&#xff0c;mysql数据库&#xff0c; idea工具开发&#xff0c;本系统分为用户&#xff0c;户主&#xff0c;管理员三个角色&#xff0c;其中用户可以注册登陆小程序&#…

C++11入门

目录 C11简介 统一的列表初始化 {}初始化 std::initializer_list 文档介绍 std::initializer_list的类型 使用场景 initializer_list接口函数模拟实现 auto与decltype nullptr 范围for STL的变化 新容器 新方法 新函数 C11简介 1.在2003年C标准委员会曾经提交了一…

【浅学Redis】缓存 以及 缓存穿透、缓存击穿、缓存雪崩

缓存 以及 缓存击穿、缓存穿透、缓存雪崩1. 缓存1.1 缓存的作用1.2 缓存的应用场景1.3 引入缓存后的执行流程1.4 缓存的优点2. 缓存穿透2.1 场景2.2 解决策略1. 参数校验2. 缓存空值3. 缓存击穿3.1 场景3.2 解决策略4. 缓存雪崩4.1 场景4.2 解决策略5. 上面三者的区别1. 缓存 …

图像分割--入门了解

一. 三种分割 1. 语义分割&#xff08;semantic segmentation&#xff09; 语义分割&#xff1a;语义分割通过对图像中的每个像素进行密集的预测、推断标签来实现细粒度的推理&#xff0c;从而使每个像素都被标记为一个类别&#xff0c;但不区分属于相同类别的不同实例。 比…

ChatGPT之父:世界正被他搅动

阿尔特曼&#xff08;左&#xff09;与马斯克Mac LC2电脑ChatGPT这款聊天应用程序最近太火了&#xff01; 美国北密歇根大学的一名学生用它生成了一篇哲学课小论文&#xff0c;“惊艳”了教授&#xff0c;还得到了全班最高分。美国一项调查显示&#xff0c;53%的学生用它写过论…

Vue (2)

文章目录1. 模板语法1.1 插值语法1.2 指令语法2. 数据绑定3. 穿插 el 和 data 的两种写法4. MVVM 模型1. 模板语法 root 容器中的代码称为 vue 模板 1.1 插值语法 1.2 指令语法 图一 &#xff1a; 简写 &#xff1a; v-bind: 是可以简写成 &#xff1a; 的 总结 &#xff1a; …

Springboot + RabbitMq 消息队列

前言 一、RabbitMq简介 1、RabbitMq场景应用&#xff0c;RabbitMq特点 场景应用 以订单系统为例&#xff0c;用户下单之后的业务逻辑可能包括&#xff1a;生成订单、扣减库存、使用优惠券、增加积分、通知商家用户下单、发短信通知等等。在业务发展初期这些逻辑可能放在一起…

【23种设计模式】创建型模式详细介绍

前言 本文为 【23种设计模式】创建型模式详细介绍 相关内容介绍&#xff0c;下边具体将对单例模式&#xff0c;工厂方法模式&#xff0c;抽象工厂模式&#xff0c;建造者模式&#xff0c;原型模式&#xff0c;具体包括它们的特点与实现等进行详尽介绍~ &#x1f4cc;博主主页&…