learn C++ NO.19——二叉搜索树

news2024/11/15 21:52:39

简单介绍一下二叉搜索树

二叉搜索树也称为二叉排序树。它是一种具有特殊性质的二叉树。它有如下性质。
1、当前节点的左子树的值一定小于当前节点,当前节点的右子树的值一锭大于当前节点。这也就意味着,在接近完全二叉树的情况下(高度较为合适的情况下),二叉搜索树的查找效率极高(O(LogN))。但是,当极端请况下,可能就会退化成链表。所以,二叉搜素树查找的最坏实践复杂度是O(N)。
2、并且当前节点的左子树和右子树都符合二叉搜索树的性质。
3、中序遍历有序性。二叉搜素树的中序遍历是一个升序序列。

为什么学二叉搜索树

在前面二叉树的博客中,提到了二叉树的增删查改没有意义。但是搜索二叉树不一样,它的增删查改是有意义的。并且学习二叉搜索树是为了后面的AVL树和红黑树打下基础。

模拟实现一份二叉搜索树

插入接口的实现

这里用的是循环的方式遍历树。实现的思路如下,首先,需要对第一次插入做一个特殊处理。即当根节点为空时,直接new 一个节点赋值给根节点。若不是第一次插入,那么则需要遍历树,根据搜索二叉树的性质找到合适的位置插入,需要注意的是,由于适合插入的位置一定是空节点,所以需要保存一下需要插入节点的父亲节点。这样才能让新插入的节点连接到树上。
在这里插入图片描述
下面,再实现一下递归版本的插入接口。整体还是以封装一份递归子函数来实现主体逻辑。递归版本的核心就是子函数的函数头的设计,即bool _InsertR(Node*& root, const K& key)。将第一个参数设置成实参的别名,这样修改形参root其实就是修改实参。这样的话就不用管插入的位置是在父节点的左子树还是右子树了。

递归出口就是插入数据的位置,当root走到空就插入数据并返回true。

递归主体就是当插入的值比当前节点的值大,递归去右子树找到合适的位置插入。当插入的值比当前节点的值小,递归去左子树找到合适的位置插入。当插入的值等于当前节点值,说明该元素存在,返回false。

在这里插入图片描述

中序遍历的实现

采用子函数递归的方式实现,因为将根节点设置成了私有成员,类外不可用。所以提供了一个无参的主函数调用类内子函数。中序其实就是先遍历左子树,访问根,再去右子树遍历。
在这里插入图片描述

查找接口的实现

根据搜索二叉树的特性进行一次遍历即可。当前节点的值大于要查找的值时,去当前节点的左边查找。若当前节点的值小于要查找的值时,去当前节点的右边查找。若当前节点的值等于要查找的值时,直接返回true。当遍历结束后还没找到则返回false。
在这里插入图片描述
下面实现一份递归版本的代码,具体的实现思路如下,依旧采用对外提供一个主函数,在类内写一个递归子函数实现查找的逻辑。

参数部分这么设计,主函数的函数头为 bool FindR(const K& key)。递归子函数的函数头为bool _FindR(Node* root, const K& key) 。

递归子函数主体逻辑实现思路如下,递归出口为当root为空时,则表示当前查找的值不存在。递归主体为当root的值比要找的值小时,去root的右子树找。当root的值比要找的值大时,去root的左子树找。当root的值等于要找的值时,返回 true。
在这里插入图片描述

删除接口的实现

删除接口也是二叉搜索树的最核心接口。实现起来比较的复杂,下面且听我分析,由于找到带删除元素都逻辑上面已经实现过了,这里不再赘述。下面直接讲删除的逻辑。

首先,删除的情况有三种,分别是删除的这个节点是叶子节点、删除的节点的左子树或右子树为空以及删除的这个节点既有左子树又有右子树。
在这里插入图片描述
其实第1种情况和第2种情况其实是可以归为一类来看的。因为这两种情况本质上都是将被删除节点左右子树关系托孤给被删除节点的父节点。这里我就以托孤法来形容这种情况。

第3种情况既可以从右子树中找到合适的节点替换,也可以从左子树中找到合适的节点替换。然后删除8这个位置,以保持二叉搜索树的特性。从右子树找最小的值与8替换或左子树找最大的值与8替换都可以。

下面先讲解托孤法,通过样例来进行讲解
在这里插入图片描述

假设需要删除cur这个节点。此时cur的左子树为空,此时意味着需要将cur的右子树部分托孤给parent。具体操作就是将cur->right 赋值给parent->left 。然后删除cur。

下面再看一个特殊的托孤法的场景
在这里插入图片描述
由于cur就是根节点,意味着我们需要对这种情况进行特殊的处理。直接修改root的值为cur的右子树。

那么托孤法主要逻辑是判断当前节点的左子树为空还是右子树为空的场景作为切入点。而叶子节点恰好符合,所以,可以在这个逻辑里面进行处理。

下面提供代码,可以分别带入上面给的样例感受一下。
在这里插入图片描述

在这里插入图片描述

下面讲解一下左右都不为空的处理逻辑。以从左子树中找最大值为例,那就是定义一个变量leftMax以根节点的左子树进行赋值,再保存leftMax的父节点。然后依次遍历leftMax的右子树,便可以找到左子树的最大值。不过需要注意一个特殊情况,就是leftMax一开始就是左子树的最大值。这个逻辑需要单独处理。当找到左子树的最大值后,用左子树的最大值和被删除的元素的值进行交换。然后修改父节点的连接关系即可。

在这里插入图片描述
在这里插入图片描述

//删除接口——迭代版本
bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;

	while (cur)
	{
		//比cur的值小
		if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		//比cur的值大
		else if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else //找到了 
		{
			//大体分两种情况,1、托孤 2、找子树的最大值交换
			//左边为空的情况
			if (cur->_left == nullptr)
			{
				//边界情况
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					// 需要考虑cur所处的位置
					if (parent->_left == cur)
					{
						parent->_left = cur->_right;
					}
					else
					{
						parent->_right = cur->_right;
					}
				}
			}
			else if (cur->_right == nullptr)//右为空
			{
				//边界情况
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else 
				{
					// 需要考虑cur所处的位置
					if (parent->_left == cur)
					{
						parent->_left = cur->_left;
					}
					else
					{
						parent->_right = cur->_left;
					}
				}
			}
			else//左右不为空,与左子树的最大值交换
			{
				//这里parent不能为null,否则下面特殊情况会出错
				parent = cur;
				Node* leftMax = cur->_left;
				while (leftMax->_right)
				{
					parent = leftMax;
					leftMax = leftMax->_right;
				}

				swap(leftMax->_key, cur->_key);


				//特殊情况 _root->left == leftMax
				if (parent->_left == leftMax)
				{
					parent->_left = leftMax->_left;
				}
				else
				{
					parent->_right = leftMax->_left;
				}

				//符合统一删除逻辑
				cur = leftMax;
			}
			//统一删除逻辑
			delete cur;
			return true;
		}
	}
	return false;
}

下面再介绍递归版本的实现。非递归版本相比于递归版本来说是比较复杂的,但是实践当中还是推荐使用非递归版本代码。因为递归有栈溢出的风险。回到正题,下面实现递归版本代码。

首先递归的实现思路如下,先设计递归子函数的函数头,这里就说一下函数头的设计,由于删除接口会修改节点的指向,所以形参部分依旧是选择节点的指针的引用进行。bool _EraseR(Node*& root, const K& key)。主函数部分依旧是和上面的递归主函数的方案保持一致。

递归的出口部分为当root为空时,表示key不存在,直接返回false。

递归主体逻辑为当root的值比要找的值小时,去root的右子树找。当root的值比要找的值大时,去root的左子树找。当root的值等于key时,开始执行删除逻辑。首先,需要将当前节点保存一份。避免执行下面逻辑导致节点丢失,进而引发内存泄漏。删除逻辑大体分为托孤(左为空或右为空的情况)和 与左子树的最大值交换后删除(左右不为空)。

左为空和右为空的情况直接修改root的指向即可。因为这里传参传的是实参的别名,形参部分是的类型是实参的引用就可以改变实参的指向。

左和右都不为空情况下,和左子树的最大值进行交换后,再次递归从root->left的位置开始,删除key。最后,进入函数后会执行托孤部分的逻辑,将节点删除。
在这里插入图片描述

在这里插入图片描述

析构函数的实现

采用子函数递归的方式,后序遍历进行依次释放节点并将节点置空。在析构函数内部调用这个子函数即可。需要注意的是子函数的函数头的参数部分,用节点指针的引用可以改变节点的指向。
在这里插入图片描述

拷贝构造的实现

拷贝构造实现思路如下,使用一个子函数递归前序遍历依次完成节点拷贝和链接。再实现一个现代写法的拷贝赋值运算符重载即可。
在这里插入图片描述

二叉搜索树的应用场景

通常二叉搜索树有两种模型,一种是key的模型,另一种是key,value模型。上面,模拟实现实现的就是一个key模型的二叉搜索树。那这两者有什么区别呢?

key模型的二叉搜索树通常用于快速判断一个key在不在的场景。比如说校园卡刷门禁系统。此时key就是你的学号,门禁系统根据你的学号判断你是否是在校生,是就放行。

key,value模型的二叉搜索树是一个根据key值去找对应的value值的一个搜索模型。比如说一个英文词典程序,通过英文单词,去词库系统中找匹配的中文翻译。

简单搭一个key value模型的二叉搜索树

通过上面手撕的key模型的二叉搜索树,我们简单进行一下修改便可以把它改造成key value结构的二叉搜索树。

namespace xyx_kv
{
	template<class K, class V>
	struct BSTNode
	{

		BSTNode(const K& key, const V& value)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}



		BSTNode* _left;
		BSTNode* _right;
		K _key;
		V _value;
	};

	template<class K, class V>
	class BSTree
	{
	public:
		typedef BSTNode<K, V> Node;
		BSTree()
			:_root(nullptr)
		{}

		BSTree(const BSTree<K, V>& t)
		{
			_root = Copy(t._root);
		}

		BSTree<K, V>& operator=(BSTree<K, V> t)
		{
			swap(_root, t._root);
			return *this;
		}

		~BSTree()
		{
			Destroy(_root);
		}



		void InOrder()
		{
			_inOrder(_root);
			cout << endl;
		}

		Node* FindR(const K& key)
		{
			return _FindR(_root, key);
		}

		bool InsertR(const K& key, const V& val)
		{
			return _InsertR(_root, key, val);
		}

		bool EraseR(const K& key)
		{
			return _EraseR(_root, key);
		}

	private:
		void Destroy(Node*& root)
		{
			if (root == nullptr)
				return;

			Destroy(root->_left);
			Destroy(root->_right);
			delete root;
			root = nullptr;
		}

		Node* Copy(Node* root)
		{
			//递归出口
			if (root == nullptr)
				return nullptr;

			//前序遍历以此拷贝
			Node* newNode = new Node(root->_key, root->_value);
			newNode->_left = Copy(root->_left);
			newNode->_right = Copy(root->_right);
			return newNode;
		}


		bool _EraseR(Node*& root, const K& key)
		{
			//key 不存在
			if (root == nullptr)
				return false;

			if (root->_key > key) //比root小,去root的左子树找
				return _EraseR(root->_left, key);
			else if (root->_key < key) //比root大,去root的左子树找
				return _EraseR(root->_right, key);
			else //找到了
			{
				// 1、左为空
				// 2、右为空
				// 3、左右不为空
				Node* del = root;//保存以便释放

				if (root->_left == nullptr)
				{
					root = root->_right;
				}
				else if (root->_right == nullptr)
				{
					root = root->_left;
				}
				else
				{
					Node* leftMax = root->_left;

					while (leftMax->_right)
					{
						leftMax = leftMax->_right;
					}

					swap(root->_key, leftMax->_key);

					return _EraseR(root->_left, key);
				}
				delete del;
				return true;
			}
		}

		bool _InsertR(Node*& root, const K& key, const V& val)
		{
			if (root == nullptr)
			{
				root = new Node(key, val);
				return true;
			}

			if (root->_key > key)
			{
				return _InsertR(root->_left, key, val);
			}
			else if (root->_key < key)
			{
				return _InsertR(root->_right, key, val);
			}
			else
			{
				return false;
			}
		}

		Node* _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
				return nullptr;

			if (root->_key > key)
			{
				return _FindR(root->_left, key);
			}
			else if (root->_key < key)
			{
				return _FindR(root->_right, key);
			}
			else
			{
				return root;
			}
		}

		void _inOrder(Node* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << ":" << root->_value << endl;
			_InOrder(root->_right);
		}

	private:
		Node* _root;
	};

下面简单写一个英文词典简易程序带大家感受一下key value搜索模型的二叉搜索树。
在这里插入图片描述

下面再看一个根据经典的key value模型的应用,统计string类数据出现的次数。
在这里插入图片描述
介绍完了key 模型和 key value模型的二叉搜索树。其实在实际使用中库里面有现成的方案可供我们使用。key模型对应的是set,key value模型对应的是map。有些许不同的是它们底层用的是红黑树(平衡二叉搜索树)来实现的。后面还会详细介绍。

总结

二叉搜索树是一种特殊的二叉树。它不仅严格要求当前节点的左子树的值小于当前节点的值,还严格要求当前节点的右子树的值大于当前节点的值。它的搜索模型有两种分别是key模型和key value 模型。对应了快速判断在不在的场景和根据关键值辅助查找数据的场景。

学习二叉搜索树的意义在于对于一些查找算法有了新的理解。和二分查找相比搜索二叉树在实景使用中更加优秀,虽然二分查找看起来很快,但是在实践中其实使用场景比较局限,二分查找需要数据具有二义性(有序性其实就是二义性)。而搜索二叉树相比于二分查找使用场景更多。

虽然在理想情况下(接近完全二叉树),二叉搜索树的查找效率是O(LogN)的。但是,二叉搜索树也伴随着特殊情况下,高度过高导致查找效率退化至O(N)。所以,可以认为二叉搜索树的查找的实际时间复杂度为O(N)。那如何让搜索二叉树的结构始终处于一个理想的状态呢?那就要引入平衡因子的概念,是二叉搜索树升级成AVL树和红黑树。这在后续的文章中在详细的介绍了。

好了本篇文章就到这里,感谢您的阅读!如有错误请指出。参考代码,点击跳转

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

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

相关文章

开源实时多模态AI聊天机器人Moshi,语音对话延迟低至200毫秒!

开源实时多模态AI聊天机器人Moshi&#xff0c;语音对话延迟低至200毫秒&#xff01; 最近AI圈真是热闹非凡&#xff0c;继Meta发布Llama 3之后&#xff0c;各种开源大模型也是层出不穷。这不&#xff0c;法国一个非盈利AI研究实验室Kyutai&#xff0c;又搞了个大新闻&#xff0…

从零到一:如何用Ollama和OpenUI构建强大的AI模型库

搭建开源大模型平台的步骤与模型介绍 在这篇文章中&#xff0c;我将分享如何在Windows上使用Ollama和OpenUI搭建开源大模型平台的步骤&#xff0c;并介绍我所部署的几个模型及其擅长的领域。 目录 搭建开源大模型平台的步骤与模型介绍一、搭建平台步骤1. 安装Ollama2. 安装Ope…

C++自动驾驶面试核心问题整理

应用开发 概述&#xff1a;比较基础&#xff0c;没啥壁垒&#xff0c;主要有linux开发经验即可 问题&#xff1a;基础八股&#xff0c;如计算机网络、操作系统、c11等基础三件套&#xff1b;中等难度算法题1-2道。 中间件开发&#xff08;性能优化&#xff09; 概述&am…

FutureTask源码分析

Thread类的run方法返回值类型是void&#xff0c;因此我们无法直接通过Thread类获取线程执行结果。如果要获取线程执行结果就需要使用FutureTask。用法如下&#xff1a; class CallableImpl implements Callable{Overridepublic Object call() throws Exception {//do somethin…

信息安全工程师(12)网络攻击概述

前言 网络攻击&#xff08;Cyber Attacks&#xff0c;也称赛博攻击&#xff09;是指针对计算机信息系统、基础设施、计算机网络或个人计算机设备的任何类型的进攻动作。这些攻击旨在破坏、揭露、修改、使软件或服务失去功能&#xff0c;或在未经授权的情况下偷取或访问计算机数…

超详细超实用!!!AI编程之cursor编写一个官网(二)

云风网 云风笔记 云风知识库 一、新建html文件 选中添加index.html,输入编写官网要求&#xff0c;自动生成代码&#xff0c;先来个简单的。 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"…

WPS2019 数据透视表多列数据如何显示同一行

在excel表格中&#xff0c;只有行筛选&#xff0c;没有列筛选功能&#xff0c;当我们需要只选取某些列的数据时&#xff0c;使用数据透视表是个可行的方法&#xff0c;但默认生成的数据透视表可观性较差。要如何才能使得数据透视表格式与原来数据格式一样美观易看呢&#xff1f…

Leetcode990.等式方程的可满足性

题目 原题链接 等式方程的可满足性 思路 定义一个长度为26&#xff08;变量为小写字母&#xff09;的数组充当并查集&#xff0c;并将数组中的元素初始化为 -1判断“”并合并元素&#xff0c;将相等的放在一个集合中判断“!”&#xff1b;不等的如果在一个集合中&#xff0c;则…

【Linux】指令和权限的这些细节,你确定都清楚吗?

&#x1f680;个人主页&#xff1a;奋斗的小羊 &#x1f680;所属专栏&#xff1a;Linux 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 前言&#x1f4a5;一、Linux基本指令&#x1f4a5;1.1 mv 指令&#x1f4a5;1.2 cat 指令&#x1f4a5;…

webLogic反序列化漏洞CVE-2017-3506

1.环境搭建 cd vulhub-master/weblogic/weak_password docker-compose up -d 2.判断wls-wsat组件是否存在 拼接/wls-wsat/CoordinatorPortType 查看页面是否有回显 有回显说明存在组件 3.在当前页面抓包 反弹shell 添加请求包内容 <soapenv:Envelope xmlns:soapenv&q…

hCaptcha 图像识别 API 对接说明

hCaptcha 图像识别 API 对接说明 本文将介绍一种 hCaptcha 图像识别 API 对接说明&#xff0c;它可以通过用户输入识别的内容和 hCaptcha验证码图像&#xff0c;最后返回需要点击的小图像的坐标&#xff0c;完成验证。 接下来介绍下 hCaptcha 图像识别 API 的对接说明。 注册…

线程的状态及join()插队方法

一、线程的状态 线程整个生命周期中有6种状态&#xff0c;分别为 NEW 新建状态 、RUNNABLE 可运行状态、TERMINATED 终止状态、TIMED_WAITING计时等待状态、WAITING 等待状态、BLOCKED 阻塞状态 线程各个状态之间的转换&#xff1a; 在 JAVA 程序中&#xff0c;一个线程对象通过…

一文搞懂offset、client、scroll系列及案例

目录 一、offset 1-1、offset系列属性 1-2、offset与style区别 1-3、案例 1-3-1、计算鼠标在盒子内的坐标 1-3-2、拖动模态框 二、client 2-1、client系列属性 三、scroll 3-1、scroll系列属性 3-2、案例 3-2-1、滚动页面一定距离后固定侧边栏 一、offset offset是…

pg入门3—详解tablespaces—下

pg默认的tablespace的location为空&#xff0c;那么如果表设置了默认的tablespace&#xff0c;数据实际上是存哪个目录的呢? 在 PostgreSQL 中&#xff0c;如果你创建了一个表并且没有显式指定表空间&#xff08;tablespace&#xff09;&#xff0c;或者表空间的 location 为…

数据库数据恢复—SQL Server附加数据库出现“错误823”怎么恢复数据?

SQL Server数据库故障&#xff1a; SQL Server附加数据库出现错误823&#xff0c;附加数据库失败。数据库没有备份&#xff0c;无法通过备份恢复数据库。 SQL Server数据库出现823错误的可能原因有&#xff1a;数据库物理页面损坏、数据库物理页面校验值损坏导致无法识别该页面…

【靶点Talk】免疫检查点争夺战:TIGIT能否超越PD-1?

曾经的TIGIT靶点顶着“下一个PD-1”的名号横空出世&#xff0c;三年的“征程”中TIGIT走过一次又一次的失败&#xff0c;然而面对质疑和压力仍有一批公司选择前行。今天给大家分享TIGIT靶点的相关内容&#xff0c;更多靶点科普视频请关注义翘神州B站和知乎官方账号。 TIGIT的“…

C#和数据库高级:虚方法

文章目录 一、抽象方法和抽象类中的思考1.1、回顾抽象方法的特点1.2、针对抽象方法问题的引出 二、虚方法的使用步骤2.1、虚方法重写方法的调用2.2、系统自带的虚方法2.3、重写Equals方法2.4、虚方法和抽象方法的比较 三、虚方法和抽象方法的联系3.1、ToString()方法的应用 一、…

2024/9/23 leetcode 25题 k个一组翻转链表

目录 25.k个一组翻转链表 题目描述 题目链接 解题思路与代码 25.k个一组翻转链表 题目描述 给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff0c;请你返回修改后的链表。 k 是一个正整数&#xff0c;它的值小于或等于链表的长度。如果节点总数不是 k 的…

Gartner:中国企业利用GenAI提高生产力的三大策略

作者&#xff1a;Gartner高级首席分析师 雷丝、Gartner 研究总监 闫斌、Gartner高级研究总监 张桐 随着生成式人工智能&#xff08;GenAI&#xff09;风靡全球&#xff0c;大多数企业都希望利用人工智能&#xff08;AI&#xff09;技术进行创新&#xff0c;以收获更多的业务成果…

JS 历史简介

目录 1. JS 历史简介 2. JS 技术特征 1. JS 历史简介 举例&#xff1a;在提交用户的注册信息的时候&#xff0c;为避免注册出现错误后重新填写信息&#xff0c;可以在写完一栏信息后进行校验&#xff0c;并提示是否出现错误&#xff0c;这样会大大提高用户提交的成功率&…