搜索二叉树BSTree的原理及实现

news2024/11/15 17:53:47

目录

一、简介

二、功能的实现

节点的实现

这里为什么模板参数采用的是K而不是T呢?

树体的实现

非递归版本

Insert函数

Find函数

Erase函数

递归版本

中序遍历

FindR

InsertR

EraseR

构造函数

析构函数

拷贝构造

赋值重载


一、简介

BSTree(Binary Search Tree),即二叉搜索树,是一种特殊的二叉树,具有以下特性:

  1. 节点的左子树上所有节点的值均小于它的根节点的值:这意味着在二叉搜索树中,任何一个节点的左子树中的元素都是小于该节点的。

  2. 节点的右子树上所有节点的值均大于它的根节点的值:同样,任何一个节点的右子树中的元素都是大于该节点的。

  3. 左右子树也分别为二叉搜索树:二叉搜索树的每一个子树也是二叉搜索树。

  4. 没有键值相等的节点:在二叉搜索树中,所有节点的值都是唯一的。

二叉搜索树具有以下优点:

  • 高效的查找、插入和删除操作:在二叉搜索树上进行查找、插入和删除操作的时间复杂度平均为O(log n),其中n是树中节点的数量。

  • 保持数据的有序性:二叉搜索树的中序遍历结果是有序的,即按照从小到大的顺序排列。

二叉搜索树的操作包括:

  • 查找:从根节点开始,比较当前节点与目标值的的大小,根据比较结果决定是向左子树还是右子树递归查找。

  • 插入:从根节点开始,比较当前节点与待插入值的大小,找到合适的位置插入新节点。

  • 删除:删除操作较为复杂,需要考虑三种情况:

    • 删除的节点是叶子节点,可以直接删除。
    • 删除的节点只有一个子节点,可以用其子节点代替该节点。
    • 删除的节点有两个子节点,通常找到该节点的中序后继(右子树中的最小节点)或中序前驱(左子树中的最大节点)来代替,然后删除该后继或前驱节点。

下图就是一棵根据数组建成的搜索二叉树

下面我们就根据搜索二叉树的特点及功能进行二叉树的实现

二、功能的实现

跟普通的树结构一样,我们需要两个自定义类型来实现BSTree的功能。首先定义一个节点,用来存放树的键位,其次另一个自定义类型进行树功能接口的实现和树的搭建。

在这里我们并没有单独的放到一个命名空间中,主要是因为跟std的命名冲突不大,所以我们直接在全局就可以进行实现。

节点的实现

template<class K>
struct BSTreeNode
{
	BSTreeNode<K>* _left;	//模板实例化之后才是类型
	BSTreeNode<K>* _right;
	K _key;

	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};

由于我们希望节点部分对于整个树部分是公开的希望可以访问他的_left等内容,所以我们采用struct结构,而不是class类。

需要注意的是    BSTreeNode<K>*模板实例化之后才是类型。

这里为什么模板参数采用的是K而不是T呢?

以下是使用K作为模板参数的几个原因:

明确性:K暗示了模板参数代表的是键(Key),这在使用二叉搜索树时,键是用来比较和排序的主要元素。

一致性:在涉及键值对或键相关的数据结构中,使用K作为键的类型可以保持命名的一致性,使得代码在不同的上下文中易于理解和维护。

约定:在许多编程实践中,T通常用于表示“Type”,是一个通用的类型占位符。但在特定的情况下,使用更具体的字母可以提供更多的上下文信息。例如,V可能用于表示“Value”,E可能用于表示“Element”等。

避免混淆:如果代码中已经使用了T作为其他意义下的模板参数,为了避免混淆,可能会选择其他字母。

总的来说,选择K而不是T是为了更好地传达模板参数的意图,并且遵循了类型参数命名的通用约定。这样的命名习惯有助于其他开发者快速理解代码的结构和用途

树体的实现

成员参数:

由于树是由一个个节点组成的,所以参数用一个根节点构成。

private:
	Node* _root = nullptr;		//类内初始化,将一个根节点置空

为了方便使用类型,进行一次typedef

template<class K>
class BSTree
{
public:
	typedef BSTreeNode<K> Node;

功能函数的实现

功能函数重点是插入、删除、遍历、修改。由于树可以进行递归实现,因此我们实现了递归与非递归版本的实现。

非递归版本

Insert函数

用来完成插入与树的构建操作。

插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
其性质指的是左树<键值         右树 > 键值
bool Insert(const K& key)	//不允许值重复
{
	Node* cur = _root;
	Node* prev = cur;		//定义prev用来进行节点的链接

	if (cur == nullptr)		//空树
		_root = new Node(key);		//直接对根节点进行链接,而不是新建立newnode

	while (cur)			//非空树
	{
		if (key > _root->_key)
		{
			prev = cur;
			cur = cur->_right;
		}

		else if (key < _root->_key)
		{
			prev = cur;
			cur = cur->_left;
		}

		else
			return false;
	}

	//cur->_key = key;		//错误,cur目前是一个指向未知区域的野指针

	cur = new Node(key);	//应该连入一个新节点
	
	//链接

	if (prev->_key > key)
		prev->_left = cur;

	else
		prev->_right = cur;

	return true;
}

第一部分是判断该树是不是空树。让_root指向一个新开辟的节点即可。

第二部分是找到该树的空节点。之后是借助key新建一个节点。

第三部分是判断父子的位置关系,进行数据的链接。

Find函数

用于查找数据,左树小于键值,右树大于键值。

时间复杂度:logn:接近满二叉树(完全二叉树)

                    n:最坏(最坏的才是时间复杂度)

	bool Find(const K& key)		//给值检索
	{
		Node* cur = _root;

		while (cur)
		{
			if (key > cur->_key)
				cur = cur->_right;

			else if (key < cur->_key)
				cur = cur->_left;

			else
				return true;
		}

		return false;
	}

Erase函数

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程
如下:
情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点
中,再来处理该结点的删除问题--替换法删除
情况bc是“托孤法” 形况d是替换法
替换时找左树的最大节点或者右树的最小节点
	bool Erase(const K& key)
	{
		Node* cur = _root;
		Node* prev = cur;
		
		while (cur)
		{

			if (key > cur->_key)
			{
				prev = cur;
				cur = cur->_right;
			}

			else if (key < cur->_key)
			{
				prev = cur;
				cur = cur->_left;
			}

			else	//找到了,进行删除
			{	
				if (cur->_left == nullptr)				//左孩子为空
				{	
						//当节点是根时(此时不存在父节点)
					if (cur == _root)
						_root = _root->_right;

						//判断父子关系
					if (prev->_key > cur->_key)
						prev->_left = cur->_right;	//托孤法

					else if (prev->_key < cur->_key)
						prev->_right = cur->_right;

					delete cur;		//最终都是删除cur,可以直接写道最后

				}

				else if (cur->_right == nullptr)		//右孩子为空
				{	
					//当节点是根时(此时不存在父节点)
					if (cur == _root)
						_root = _root->_left;

						//判断父子关系
					if (prev->_key > cur->_key)
						prev->_left = cur->_left;	//托孤法

					else if (prev->_key < cur->_key)
						prev->_right = cur->_left;

						delete cur;
				}

				else	//左右孩子都有:替换法
				{
						// 右树的最小节点(即最左节点,最左节点一定没有左孩子,但是可能有右孩子)
					Node* parent = cur;		//定义一个父节点去链接
					Node* MinRight = cur->_right;	//去寻找右树最小的节点

					while (MinRight->_left)		//在此处不可能出现根的左右都是空的情况(想删除时,前两种情况已经对此做出了处理)
					{
						parent = MinRight;
						MinRight = MinRight->_left;
					}

					swap(cur->_key, MinRight->_key);	//本质是对键值交换

					if (parent->_left = MinRight)		
						parent->_left = MinRight->_right;

					else
						parent->_right = MinRight->_right;

					delete MinRight;

				}

				return true;		//删除成功之后返回true

			}


		}

		return false;
	}

需要注意的是:

1.考虑root是不是需要删除的对象

2.交换时,本质是对键值的交换

3.在链接时,考虑父子的关系

递归版本

中序遍历

中序是一个递增序列(可以实现有序)

二叉树的递归都需要传入根,在外面传不合适,但是可以在内部传入。但是C++不喜欢写Get()方法,因此需要对递归的函数进行一次封装。

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

	_Inorder(root->_left);
	cout << root->_key << " ";
	_Inorder(root->_right);
}

void类型不需要返回,直接及逆行递归遍历即可。

FindR

递归版本的查找

由于是bool类型,应该也有一次返回。

递归函数

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

	if (key > root->_key)
	{
		_FindR(root->_right, key);
	}

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

	else
		return true;
}

InsertR

	bool _InsertR(Node*& root, const K& key)	//引用传参
	{
		if (root == nullptr)
		{
			root = new Node(key);		//引用传参,保证_root可以链接
			return true;
		}

		if (key > root->_key)
			_InsertR(root->_right, key);

		else if (key < root->_key)
			_InsertR(root->_left, key);

		else	//相等
			return false;
	}

需要注意的是,我们需要传入的是指针的引用,因为我们需要修改一级指针。

在子问题函数中root就是上一级问题的root->_left   root->_right

因此这句代码:            root = new Node(key);        //引用传参,保证_root可以链接

本质就是上一层的root->_left  /  root->_right = new Node(key)

EraseR


	bool _EraseR(Node*& root, const K& key)		//指针的引用可以修改一级指针
	{	
		if (root == nullptr)
			return false;

		if (key > root->_key)
			_EraseR(root->_right, key);

		else if (key < root->_key)
			_EraseR(root->_left, key);

		else	//找到了,进行删除
		{
			if (root->_left == nullptr)				//左孩子为空
			{
				//不需要判断父子关系(引用传参,知道root就是上一次的_left或者_right)因此不需要定义prev指针

				Node* del = root;
				root = root->_right;	//根节点也可以完成删除
				delete del;			//不能delete root

				return true;
			}

			else if (root->_right == nullptr)		//右孩子为空
			{
				Node* del = root;
				root = root->_left;	//根节点也可以完成删除
				delete del;			//不能delete root

				return true;
			}

			else	//左右孩子都有:替换法
			{
				Node* MinRight = root->_right;
				while (MinRight->_left)
				{
					MinRight = MinRight->_left;
				}

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

				return _EraseR(root->_right, key);		//左右孩子都有这种情况要递归,必须有一次return
					//不能传入MinRight,因为他的父亲不能链接他的孩子(目的是为了托孤)
			}

			//return true;		不需要额外返回一次

		}

	}

核心逻辑:

if (root->_left == nullptr)				//左孩子为空
{
	//不需要判断父子关系(引用传参,知道root就是上一次的_left或者_right)因此不需要定义prev指针

	Node* del = root;
	root = root->_right;	//根节点也可以完成删除
	delete del;			//不能delete root

	return true;
}

else if (root->_right == nullptr)		//右孩子为空
{
	Node* del = root;
	root = root->_left;	//根节点也可以完成删除
	delete del;			//不能delete root

	return true;
}

else	//左右孩子都有:替换法
{
	Node* MinRight = root->_right;
	while (MinRight->_left)
	{
		MinRight = MinRight->_left;
	}

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

	return _EraseR(root->_right, key);		//左右孩子都有这种情况要递归,必须有一次return
		//不能传入MinRight,因为他的父亲不能链接他的孩子(目的是为了托孤)
}

第一二种情况中,不需要判断root是否为根节点,即使为跟节点,也可以完成删除。

由于是引用传参,root可以作为一级指针就可以完成节点的链接。

第三种情况中,交换的本质还是交换键值,转化成去右子树删除对应的键值(找的是右子树的最小节点)

不能直接传入MinRight,前面的传参都是root->_left这种形式,保证可以直接找到父节点进行链接,直接传入一个节点无法完成爷孙(MinRight)节点的链接。这个地方的递归要右一次return,层层return回去。

return _EraseR(root->_right, key);    也可以改为 _EraseR(root->_right, key);    return true;

构造函数

	BSTree() = default;		//C++11

可以写成BST()  {}。也可以直接让BST() = default。这个是C++11才出现的语法,表示的意思是要求编译器为BST类生成默认的构造函数。

析构函数


	~BSTree()
	{
		Destroy(_root);
	}
void Destroy(Node*& node)	//引用传参,才能让指针置空
{
	if (node == nullptr)
		return;

	Destroy(node->_left);
	Destroy(node->_right);

	delete node;
	node = nullptr;		//置空,防止出现野指针
}

引用传参,才能让指针置空。         后续结构析构,防止出现野指针。

拷贝构造

拷贝构造冲成员变量入手。

	//this(tree)
	BSTree(const BSTree<K>& t)
	{
		_root = Copy(t._root);
	}

按照先序,一次new出新阶段,进行拷贝构造。 


	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* newRoot = new Node(root->_key);	

		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);

		return newRoot;

	}

赋值重载

借助拷贝构造,完成形参的传参。内部交换跟指针,让原来的指针指向tmp二叉树,tmp二叉树跟需要拷贝构造的二叉树一致。(_root知只是指针,指向了BSTree的有效空间)

出作用域,自动调用tmp的析构,销毁原来的BSTree。

BSTree<K>& operator=(const BSTree<K> tmp)		//借助拷贝构造形成形参
{
	swap(tmp._root, _root);	//两个指针的指向交换
	return *this;		//tmp自动析构。
}

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

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

相关文章

Python 数学建模——Prophet 时间序列预测

文章目录 前言原理使用方法&#xff08;初级&#xff09;代码实例Prophet 高级应用add_seasonality 添加自定义周期性add_regressor 添加外生变量交叉检验 前言 Prophet 是 Facebook 团队开发的一个时间序列分析工具&#xff0c;相比传统的 ARMA 时间序列分析&#xff0c;能够综…

nodejs 007:错误npm error Error: EPERM: operation not permitted, symlink

完整错误信息 npm error Error: EPERM: operation not permitted, symlink npm warn cleanup Failed to remove some directories [ npm warn cleanup [ npm warn cleanup C:\\Users\\kingchuxing\\Documents\\IPFS\\orbit-db-set-master\\node_modules\\ipfs-cli, npm…

岭回归:带示例的分步介绍

由 AI 生成&#xff1a;DNA、基因组、摘要、岭回归 一、说明 岭回归是一种在独立变量高度相关的情况下估计多元回归模型系数的方法。 [ 1 ]它已用于计量经济学、化学和工程学等许多领域。[ 2 ]也称为Tikhonov 正则化&#xff0c;以Andrey Tikhonov命名&#xff0c;是一种解决不…

Lombok:Java开发者的代码简化神器【后端 17】

Lombok&#xff1a;Java开发者的代码简化神器 在Java开发中&#xff0c;我们经常需要编写大量的样板代码&#xff0c;如getter、setter、equals、hashCode、toString等方法。这些代码虽然基础且必要&#xff0c;但往往占据了大量开发时间&#xff0c;且容易在属性变更时引发错误…

【Linux取经之路】编译器gcc/g++的使用 调试器gdb的使用

目录 背景知识 编译器gcc/g的安装 编译器gcc/g的使用 调试器gdb的使用 cgdb 条件断点 背景知识 子曰&#xff1a;“温故而知新”。在谈gcc/g的使用之前&#xff0c;我们先来复习编译的4个阶段&#xff0c;也算是为下面的内容做一些铺垫&#xff0c;请看思维导图。 编译…

动态规划算法:05.路径问题_不同路径_C++

题目链接&#xff1a;LCR 098. 不同路径 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/2AoeFn/description/ 一、题目解析 题目&#xff1a; 解析&#xff1a; 由题我们可知&#xff0c;在一个网格中&#xff0c;机器人需要从左上角出发&#xff0c;…

热点数据更新优化

热点数据更新优化 热点数据更新带来的问题问题的方向判断用户线程被打挂据库cpu被打挂&#xff08;优先考虑&#xff0c;80%可能性&#xff09;redis被打挂 临时解决方案解决方案流量控制热点隔离数据分批次提交数据合并后更新重写MySQL的执行层 热点数据更新带来的问题 问题的…

ERP进销存管理系统的业务全流程 Axure高保真原型源文件分享

这是一套ERP进销存管理系统的业务全流程Axure高保真原型设计文档。 原型预览地址&#xff1a;https://ppndif.axshare.com 产品意义&#xff1a; 提高工作效率&#xff1a; 电子记账替代手工记账&#xff0c;减少工作负担和人为错误。 实时查看库存情况&#xff0c;减少盘点时…

嵌入式AI---如何用C++实现YOLO的NMS(非极大值抑制)算法

文章目录 前言一、为什么需要NMS算法&#xff1f;二、什么是NMS算法&#xff1f;三、如何使用C编写一个NMS算法1、预测框定义2、滤除无效框 总结 前言 YOLO系列的目标检测算法在边缘部署方面展现出了强大的性能和广泛的应用潜力。大部分业务场景是利用PyTorch在服务器端完成检…

细说STM32单片机通用定时器使用输出比较功能生成PWM波的方法

目录 一、本实例测试的目的 二、硬件和CubeMX项目配置 1、硬件开发板 2、项目配置 &#xff08;1&#xff09;定时器TIM2_CH1 &#xff08;2&#xff09;时钟和Debug 三、使用比较功能生成PWM 1、启动定时器 2、TIM2_CH1通道GPIO初始化 3、下载与测试 一、本实例测试…

一,掌心里的智慧:我的 TinyML 学习之旅

从云端到掌心&#xff1a;TinyML 的故事 想象一下&#xff0c;有一天你起床&#xff0c;伸手去关闭窗边的小闹钟&#xff0c;却发现这个小家伙已经提前预判到你的醒来时间——因为它能够“听到”你昨晚的呼吸变化&#xff0c;分析出你什么时候会醒。这个场景可能听起来像科幻小…

OrionX vGPU 研发测试场景下最佳实践之Jupyter模式

在上周的文章中&#xff0c;我们讲述了OrionX vGPU研发测试场景下最佳实践之SSH模式&#xff0c;今天&#xff0c;让我们走进 Jupyter模式下的最佳实践。 • Jupyter模式&#xff1a;Jupyter是最近几年算法人员使用比较多的一种工具&#xff0c;很多企业已经将其改造集成开发工…

[C++] 剖析多态的原理及实现

文章目录 多态的概念及定义编译时多态&#xff08;静态多态&#xff09;运行时多态&#xff08;动态多态&#xff09;动态多态的原理示例&#xff1a;运行时多态 两种多态的区别 多态的实现基本条件虚函数虚函数的重写与覆盖虚函数重写的其他问题协变析构函数的重写 C11 中的 o…

【数据结构】8——图3,十字链表,邻接多重表

数据结构8——图3&#xff0c;十字链表&#xff0c;邻接多重表 文章目录 数据结构8——图3&#xff0c;十字链表&#xff0c;邻接多重表前言一、十字链表结构例子 复杂例子 二、邻接多重表&#xff08;Adjacency Multilist&#xff09;例子 前言 除了之前的邻接矩阵和邻接表 …

Kubernetes部署及示例

目录 一、实验环境 二、部署 1、添加解析 2、安装docker&#xff0c;确保登录成功 3、所有禁用swap和本地解析 4、 安装K8S部署工具 5、集群初始化 6、安装flannel网络插件 7、节点扩容 三、kubernetes 中的资源 1、资源管理介绍 2、资源管理方式 &#xff08;…

【Kubernetes】服务账号 Service Account

《K8s 的安全认证》系列&#xff0c;共包含以下文章&#xff1a; K8s 的安全框架和用户认证K8s 的鉴权管理&#xff08;一&#xff09;&#xff1a;基于角色的访问控制&#xff08;RBAC 鉴权&#xff09;K8s 的鉴权管理&#xff08;二&#xff09;&#xff1a;基于属性 / 节点…

Mac导入iPhone的照片怎么删除?快速方法讲解

随着Apple生态系统的高度整合&#xff0c;Mac与iPhone之间的照片同步和导入变得异常便捷。但这种便利有时也会带来一些管理上的困扰&#xff0c;比如Mac导入iPhone的照片怎么删除&#xff1f; 从iPhone直接删除照片 Mac导入iPhone的照片怎么删除&#xff1f;如果你的照片是通…

思维商业篇(1)—如何判断商业效率

思维商业篇(1)—如何判断商业效率 我们评价一个公司&#xff0c;很大程度上其实就是看其商业效率高不高以及规模大不大。 规模是一个企业的大小&#xff0c;效率是一个企业的节奏。 一个小企业如果效率很高&#xff0c;在未来就会有很多的机会。只要其所在行业在&#xff0c…

深入理解Python中的魔法参数 *args 和 **kwargs

在Python编程中&#xff0c;函数的灵活性是其强大之处之一。其中&#xff0c;*args 和 **kwargs 是实现函数参数可变性的重要工具。 无论我们是Python初学者还是经验丰富的开发者&#xff0c;充分理解这两个概念都有助于编写更加灵活、高效的代码。 本文将深入探讨*args和**kw…

【JavaScript】数据结构之树

什么是树形结构&#xff1f; 一种分层数据的抽象模型&#xff0c;用来分层级关系的。虚拟dom它所组织的那个数据原理就是树形结构 深度优先搜索&#xff08;遍历&#xff09;- 递归 从根出发&#xff0c;尽可能深的搜索树的节点技巧 访问根节点对根节点的children挨个进行深…