数据结构进阶 二叉搜索树

news2024/11/23 21:14:58

作者:@小萌新
专栏:@数据结构进阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:介绍二叉搜索树并且模拟实现之

二叉搜索树

  • 二叉搜索树的概念
  • 节点类
  • 二叉搜索树类
    • 私有成员
    • 构造函数
    • 拷贝构造函数
    • 赋值运算符重载函数
      • 传统写法
      • 现代写法
    • 析构函数
    • 插入函数
      • 非递归实现
      • 递归实现
    • 删除函数
      • 非递归实现
      • 递归实现
    • 查找函数
      • 非递归实现
      • 递归实现
    • 二叉搜索树的性能分析

二叉搜索树的概念

二叉搜索树应当具有下面的性质

  • 空树是二叉搜索树
  • 若其左子树不为空 则其左子树上所有值小于根节点的值
  • 若其右子树不为空 则其右子树上所有制大于根节点的值
  • 其左右子树也分别是二叉搜索树

如下图

在这里插入图片描述
这就是一颗二叉搜索树

我们将其中序遍历 由于二叉搜索树的性质 得到的一定是有序的数组

节点类

为了符合C++的封装性 我们这里首先要建立一个节点类

看看上面的二叉搜索树我们就能看出来这个树需要的成员变量有哪些

一个左指针 一个右指针 还有一个就是我们的节点值

我们只需要完成一个构造函数就好

代码表示如下

template<class T>
class BSnode
{
public:
	BSnode<T>* _left;  // 左指针
	BSnode<T>* _right; // 右指针
	T _key;           // 节点值

	BSnode(const T& key = 0)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
	{}
};

我们可以创建一个节点看看

BSnode<int>* p = new BSnode<int>(10);

在这里插入图片描述
我们可以发现没有问题

二叉搜索树类

私有成员

这里的成员和二叉树一样 只需要一个_root(根节点)就可以

代码表示如下

template<class T>
class BSTree
{
	typedef BSnode<T> Node
public:

	// 成员函数
private:
	Node* _root;
};

构造函数

我们想要构造出一颗新的二叉搜索树类 只要构造出一个空的根节点就好

代码表示如下

	BSTree()
		:_root(nullptr)
	{}

拷贝构造函数

为了以让代码更加简洁 同时也更加容易阅读和理解 我们这里使用一个子函数来完成拷贝构造 (一般来说子函数要私有化 这里为了方便就不进行私有化了)

子函数写的也很简单 分别拷贝根节点 左右子树就好了

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

		Node* copynode = new Node(root->_key); // 拷贝根节点
		copynode->_left = _BSTree(root->_left);
		copynode->_right = _BSTree(root->_right);

		return copynode;
	}

	BSTree(const Node& t)
	{
		_root = _BSTree(t._root);
	}

赋值运算符重载函数

传统写法

我们要使用赋值运算符重载 只需要将给我们的树复制一份就可以

	// 赋值运算符重载 传统写法 
	const Node& operator = (const Node& t)
	{
		if (this != &t) 
		{
			_root = _BSTree(t._root);
		}
		return *this
	}

这里要注意的是 我们的那个if 条件 是为了防止自己给自己赋值

现代写法

	const Node& operator = (Node& t)
	{
		swap(_root, t._root);
		return *this;
	}

我们知道的是 形参是实参的一份临时拷贝 出作用域之后会自动析构

那么我们就可以利用这个性质 直接交换两个树的根节点

这样子就完成了 代码很简洁

析构函数

二叉树的析构函数其实就是后序遍历加上delete

至于为什么要后序遍历呢? 因为只有这样子才不会破坏二叉树的结构

	void _Destory(Node* root)
	{
		if (root == nullptr) // 遍历到空节点之后就不管了
		{
			return;
		}
		_Destory(root->_left);
		_Destory(root->_right);
		delete root;
	}
	// 析构函数
	~BSTree()
	{
		_Destory(_root); // 释放二叉树
		_root = nullptr;
	}

插入函数

这里和堆的插入差不多 我们只要理清楚二叉搜索树的概念 分两种情况讨论

  1. 假如是空树 那么插入的节点就作为它的根节点
  2. 假如不是空树 那么久按照二叉搜索树的性质进行插入

那么 什么是二叉搜索树的性质呢?

  1. 若待插入结点的值小于根结点的值,则需要将结点插入到左子树当中。
  2. 若待插入结点的值大于根结点的值,则需要将结点插入到右子树当中。
  3. 若待插入结点的值等于根结点的值,则插入结点失败。

之后按照这个性质 递归或循环实现 知道最后的值是空或者和插入的值相同则结束

非递归实现

首先我们要使用一个Cur指针来寻找到空或者是相同的那个位置

如果找到相同的值 则我们插入失败 返回一个false

如果找到空之后我们就准备开始链接

但是与此同时 我们还不知道父节点在哪里 因此我们要需要创建一个Parent指针来记录父节点的位置

	// 插入
	bool Insert(const T& key)
	{
		// 这里就写完了根节点为空的情况
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;

		// 开始找空或相同值
		while (cur)
		{
			// 如果找到了相同的值 则插入失败 
			if (key == cur->_key)
			{
				return false;
			}
			else if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				parent = cur;
				cur = cur->_right;
			}
		}

		// 开始新节点 然后判断插入哪边
		cur = new Node(key);

		if (key < parent->_key)
		{
			parent->_left = cur;
			return true;
		}
		else
		{
			parent->_right = cur;
			return true;
		}
	}

这样子我们就完成了插入操作 那么 我们应该怎么验证我们的插入操作呢?

这个时候就用到了我们上面的性质 即中序遍历后它应该是有序的

那么我们首先写出一个中序遍历的代码

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

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


	void Inorder()
	{
		_Inorder(_root);
	}

那么我们现在随机插入十个数 再用中序遍历 看看是否是升序排列的

代码和显示效果如下

	BSTree<int> b1;
	b1.Insert(3);
	b1.Insert(5);
	b1.Insert(9);
	b1.Insert(10);
	b1.Insert(1);
	b1.Insert(4);
	b1.Insert(8);
	b1.Insert(7);
	b1.Insert(2);
	b1.Inorder();

在这里插入图片描述
我们可以发现 符合预期

递归实现

递归实现的写法很简单 但是理解起来有点难 大家先看代码

	bool _InsertR(Node*& root, const T& key) // 注意这里root的引用 很重荣
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}

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

	bool Insert(const T& key)
	{
		return _InsertR(_root, key);
	}

因为上面的参数是引用 所以说像上面的

  root->_left

实际上就是root的别名 因此我们对于root赋值时候 实际上就是对于它赋值

right同理

这就是递归连接的技巧

之后我们再来验证下

在这里插入图片描述

删除函数

删除和插入一样 也是分两种情况讨论

  1. 假设没有找到我们要删除的数 则返回false表示删除失败
  2. 假设找到要删除的节点了 则要在删除完毕后还要符合二叉搜索树性质的条件下删除之

要满足条件二 则有需要满足下面三个条件之一

  1. 删除节点的左子树为空
  2. 删除节点的右子树为空
  3. 删除节点的左右子树均不为空

接下来我们对于这三种情况分别讨论下解决方案

删除节点的左子树为空

当我们要删除的节点的左子树为空的时候 (包含其右子树也为空的情况)

我们可以直接将要删除节点的parent和它的右孩子连接 (右孩子为空也成立)

之后删除掉要删除的节点就可以

删除节点的右子树为空

和上面左子树为空的情况类似 我们只需要连接父节点和子节点就好

左右节点都不为空

这是删除中最麻烦的一步

这里我们使用的方法叫做替换法

即我们可以将让待删除结点左子树当中值最大的结点 或是待删除结点右子树当中值最小的结点代替待删除结点被删除

然后将待删除结点的值改为代替其被删除的结点的值即可

接下来我们来看代码

非递归实现

	//删除函数
	bool Erase(const T& key)
	{
		Node* parent = nullptr; //标记待删除结点的父结点
		Node* cur = _root; //标记待删除结点
		while (cur)
		{
			if (key < cur->_key) //key值小于当前结点的值
			{
				//往该结点的左子树走
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key) //key值大于当前结点的值
			{
				//往该结点的右子树走
				parent = cur;
				cur = cur->_right;
			}
			else //找到了待删除结点
			{
				if (cur->_left == nullptr) //待删除结点的左子树为空
				{
					if (cur == _root) //待删除结点是根结点,此时parent为nullptr
					{
						_root = cur->_right; //二叉搜索树的根结点改为根结点的右孩子即可
					}
					else //待删除结点不是根结点,此时parent不为nullptr
					{
						if (cur == parent->_left) //待删除结点是其父结点的左孩子
						{
							parent->_left = cur->_right; //父结点的左指针指向待删除结点的右子树即可
						}
						else //待删除结点是其父结点的右孩子
						{
							parent->_right = cur->_right; //父结点的右指针指向待删除结点的右子树即可
						}
					}
					delete cur; //释放待删除结点
					return true; //删除成功,返回true
				}
				else if (cur->_right == nullptr) //待删除结点的右子树为空
				{
					if (cur == _root) //待删除结点是根结点,此时parent为nullptr
					{
						_root = cur->_left; //二叉搜索树的根结点改为根结点的左孩子即可
					}
					else //待删除结点不是根结点,此时parent不为nullptr
					{
						if (cur == parent->_left) //待删除结点是其父结点的左孩子
						{
							parent->_left = cur->_left; //父结点的左指针指向待删除结点的左子树即可
						}
						else //待删除结点是其父结点的右孩子
						{
							parent->_right = cur->_left; //父结点的右指针指向待删除结点的左子树即可
						}
					}
					delete cur; //释放待删除结点
					return true; //删除成功,返回true
				}
				else //待删除结点的左右子树均不为空
				{
					//替换法删除
					Node* minParent = cur; //标记待删除结点右子树当中值最小结点的父结点
					Node* minRight = cur->_right; //标记待删除结点右子树当中值最小的结点
					//寻找待删除结点右子树当中值最小的结点
					while (minRight->_left)
					{
						//一直往左走
						minParent = minRight;
						minRight = minRight->_left;
					}
					cur->_key = minRight->_key; //将待删除结点的值改为minRight的值
					//注意一个隐含条件:此时minRight的_left为空
					if (minRight == minParent->_left) //minRight是其父结点的左孩子
					{
						minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可
					}
					else //minRight是其父结点的右孩子
					{
						minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可
					}
					delete minRight; //释放minRight
					return true; //删除成功,返回true
				}
			}
		}
		return false; //没有找到待删除结点,删除失败,返回false
	}

递归实现

递归实现的版本和非递归实现的版本实际差别就是在查找这一步 在其他步骤上并无不同 所以我们直接写出以下代码

//递归删除函数的子函数
	bool _EraseR(Node*& root, const T& key)
	{
		if (root == nullptr) //空树
			return false; //删除失败,返回false

		if (key < root->_key) //key值小于根结点的值
			return _EraseR(root->_left, key); //待删除结点在根的左子树当中
		else if (key > root->_key) //key值大于根结点的值
			return _EraseR(root->_right, key); //待删除结点在根的右子树当中
		else //找到了待删除结点
		{
			if (root->_left == nullptr) //待删除结点的左子树为空
			{
				Node* del = root; //保存根结点
				root = root->_right; //根的右子树作为二叉树新的根结点
				delete del; //释放根结点
			}
			else if (root->_right == nullptr) //待删除结点的右子树为空
			{
				Node* del = root; //保存根结点
				root = root->_left; //根的左子树作为二叉树新的根结点
				delete del; //释放根结点
			}
			else //待删除结点的左右子树均不为空
			{
				Node* minParent = root; //标记根结点右子树当中值最小结点的父结点
				Node* minRight = root->_right; //标记根结点右子树当中值最小的结点
				//寻找根结点右子树当中值最小的结点
				while (minRight->_left)
				{
					//一直往左走
					minParent = minRight;
					minRight = minRight->_left;
				}
				root->_key = minRight->_key; //将根结点的值改为minRight的值
				//注意一个隐含条件:此时minRight的_left为空
				if (minRight == minParent->_left) //minRight是其父结点的左孩子
				{
					minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可
				}
				else //minRight是其父结点的右孩子
				{
					minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可
				}
				delete minRight; //释放minRight
			}
			return true; //删除成功,返回true
		}
	}
	//递归删除函数(方法)
	bool EraseR(const T& key)
	{
		return _EraseR(_root, key); //删除_root当中值为key的结点
	}

查找函数

这个就很好实现了 因为不管是我们上面的插入还是删除 再最前面都是用到的查找

并且是递归非递归版本都用到了 所以这里就直接写代码了

非递归实现

//查找函数
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_key) //key值小于该结点的值
		{
			cur = cur->_left; //在该结点的左子树当中查找
		}
		else if (key > cur->_key) //key值大于该结点的值
		{
			cur = cur->_right; //在该结点的右子树当中查找
		}
		else //找到了值为key的结点
		{
			return cur; //查找成功,返回结点地址
		}
	}
	return nullptr; //树为空或查找失败,返回nullptr
}

递归实现

//递归查找函数的子函数
Node* _FindR(Node* root, const K& key)
{
	if (root == nullptr) //树为空
		return nullptr; //查找失败,返回nullptr

	if (key < root->_key) //key值小于根结点的值
	{
		return _FindR(root->_left, key); //在根结点的左子树当中查找
	}
	else if (key > root->_key) //key值大于根结点的值
	{
		return _FindR(root->_right, key); //在根结点的右子树当中查找
	}
	else //key值等于根结点的值
	{
		return root; //查找成功,返回根结点地址
	}
}
//递归查找函数
Node* FindR(const K& key)
{
	return _FindR(_root, key); //在_root当中查找值为key的结点
}

二叉搜索树的性能分析

因为不管是二叉搜索树的哪个操作 实际上都会用到查找这一步 所以说二叉搜索树的性能一般来说就是查找的性能

在二叉搜索树是一颗完全二叉树的情况下 它的性能是最好的 此时它的效率为 logN

在二叉搜索树是一颗近似单边树的情况下 它的性能是最差的 此时它的效率为 N

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

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

相关文章

若想学 HTML,应从何入手?

前言 个人信息&#xff1a; 大三 工商管理 逻辑算清晰 无编程基础 想学网页设计&#xff0c;打算从HTML开始 。 下面是问题&#xff1a; 需要先学一些更基础的语言&#xff08;如C之类的&#xff09;吗&#xff1f;有何建议&#xff1a; &#xff08;1&#xff09;看哪些书、泡…

无忧·企业邮筒功能介绍

应用介绍 企业邮筒&#xff0c;基于B/S模式的邮件客户端&#xff0c;采用JVS的统一用户体系&#xff0c;作为JVS的协同办公的应用之一。 产品特点 私有化部署、支持多邮件账户、将多个邮件客户端统一为web操作、 软件架构 软件架构说明&#xff0c;JVS-mailbox是作为JVS基…

服装实体店运营需要的所有软件,合集在此!(建议收藏)实体店运营 实体店运营干货 实体店运营全流程所需系统推荐

随着信息化普及程度越来越高&#xff0c;各行各业的运转速度都在加快&#xff0c;做生意的老板们也开始发现&#xff0c;单靠以前的人工管理已经完全不够用了。 尤其是服装实体店&#xff0c;款式分类多&#xff0c;库存又容易挤压&#xff0c;更加需要有科学的手段去管控日常的…

MyBatis学习 | 缓存机制

文章目录一、一级缓存1.1 简介1.2 一级缓存的失效情况二、二级缓存2.1 简介2.2 二级缓存的使用学习地址&#x1f517; https://www.bilibili.com/video/BV1mW411M737https://www.bilibili.com/video/BV1NE411Q7Nx官网文档 一、一级缓存 1.1 简介 &#x1f4ac;概述&#xff1…

Spring与SpringBoot

目录 前言 1、Spring能做什么 1.1、Spring的能力 1.2、Spring的生态 1.3、Spring5重大升级 1.3.1、响应式编程 1.3.2、内部源码设计 2、为什么用SpringBoot 2.1、SpringBoot优点 2.2、SpringBoot缺点 3、时代背景 3.1、微服务 3.2、分布式 分布式的困难 分布式的…

迪文DGUS智能屏如何轻松实现3D动画

三维立体的视觉效果已经被广泛应用于人机交互中&#xff0c;三维图形逼真的显示效果往往可以更加直接的传递出视觉信息&#xff0c;减少用户的信息解读门槛。 传统的三维立体静态、动态画面的显示往往对于 GPU 的图像处理性能、显示带宽有较高要求&#xff0c;GPU 需要完成图形…

使用gs_probackup进行数据库物理备份与恢复

概述 物理备份与恢复适用于数据量大的场景&#xff0c;主要用于全量数据备份恢复&#xff0c;也可对整个数据库中的WAL归档日志和运行日志进行备份。openGauss提供了三种物理备份与恢复相关的工具&#xff1a;gs_backup、gs_basebackup和gs_probackup。三个工具的对比见下图。…

基于FPGA的时间数字转换(TDC)设计(二)

1、多相位TDC计时FPGA代码设计 接上期的讲解,本期主要讲多相位TDC计时的FPGA代码实现。图1为TDC测量实现系统图。时间信号经过探测器后,转换为电信号,一般探测器出来的信号幅度和脉宽都比较小,需要时间鉴别器进行比较和整形,以便于FPGA能够识别。经过FPGA TDC计时模块后,…

RabbitMQ:订阅模型-消息订阅模式

订阅模型-消息订阅模式&#xff0c;也可以称为广播模式&#xff0c;生产者将消息发送到 Exchange&#xff0c;Exchange 再转发到与之绑定的 Queue中&#xff0c;每个消费者再到自己的 Queue 中取消息。 RabbitMQ 单生产单消费模型主要有以下五个角色构成&#xff1a; 生产者&am…

机器学习10大经典算法详解

“数据算法模型”。 面对具体的问题&#xff0c;选择切合问题的模型进行求解十分重要。有经验的数据科学家根据日常算法的积累&#xff0c;往往能在最短时间内选择更适合该问题的算法&#xff0c;因此构建的模型往往更准确高效。本文归纳了机器学习的10大算法&#xff0c;并分别…

Python基础语法(一)

Python基础语法 文章目录Python基础语法基础语法变量的语法(1) 定义变量(2) 使用变量变量的类型(1) 整数(2) 浮点数(小数)(3) 字符串(4) 布尔(5) 其他动态类型特性输入输出注释通过控制台输出通过控制台输入运算符算术运算符关于除法// 取整除法关系运算符逻辑运算符关于短路求…

美格智能Cat.1无线POS终端解决方案,引领消费支付新场景

近年来&#xff0c;随着我国移动互联网的蓬勃发展和智能手机的快速渗透&#xff0c;移动支付在我国全面普及。尤其是后疫情时代下&#xff0c;无接触观念的普及&#xff0c;使我国消费市场形成了以移动支付为主的消费习惯&#xff0c;并催生了万千移动支付场景终端的数字化、智…

磁盘被写保护怎么办?5个方案解除它

硬盘、移动硬盘、U盘、SD卡和TF卡&#xff08;也称为手机存储卡&#xff09;具有写保护功能。当它们出现写保护的状态&#xff0c;我们就没有办法在里面写入数据。具体而言&#xff0c;就是无法保存和删除文件。磁盘被写保护怎么办&#xff1f;你需要下面5个方案帮助你&#xf…

20221227英语学习

今日短文 How to Become an Expert 想成为行业的专家&#xff1f;不是只花时间就够了 The drive to become expert – to become as good as we can be, at whatever we’ve chosen to do – is something we all share.It is not about external markers of success.It’s a…

01【WEB开发、Servlet】

文章目录01【WEB开发、Servlet】一、WEB开发简介1.1 什么是WEB开发1.2 软件的架构1.2.1 BS和CS概述1.2.2 WEB资源的类别1&#xff09;静态网站的特点&#xff1a;2&#xff09;动态网站的特点&#xff1a;1.3 Web服务器1.3.1 什么是服务器&#xff08;硬件&#xff09;1.3.2 什…

再也不愁渲染素材了?AI 生成3D纹理 #Polycam3D 推出新功能

最近有不少群友运用 AIGC 工具来提升工作效率&#xff0c;我听说连 3D 数字资产的渲染贴图素材都能生成了。Mixlab小杜3D 内容制作工具也是我非常感兴趣的领域&#xff0c;Polycam3D 本是一款扫描建模工具&#xff0c;近期也推出了AI生成3D纹理的功能&#xff0c;推荐大家去尝试…

启封化工行业ERP方案 ---危险化学品的备案管理

目录 危险化学品的备案管理制度 易制毒制爆危险化学品采购流程 Sage X3 ERP 危化品备案管理方案 危险化学品的备案管理制度 不少化工企业在日常的生产经营过程中&#xff0c;都有可能会涉及到易制毒、易制爆相关的危险化学品的购买和使用&#xff0c;由于易制爆、易制毒危险…

Vue组件、组件通信、路由、axios、$event、$refs、跨域代理、element-ui

文章目录{ { } }插值表达式$eventv-for删除、新增axios方法优化启动 Vue项目Vue项目的运行流程组件的三个结构组件的使用组件之间的通信父子 组件通信兄弟组件通信操作DOM插槽 slot移除node_modules路由安装、入门嵌套路由获取路由参数跨域代理element-ui表单验证Message 消息提…

基于Java+SQL Server开发(PC)学生管理系统【100010054】

题目学生管理系统 一、摘要 在当今互联网行业&#xff0c;Java 的使用及热度在各大排行榜中始终位于前列&#xff0c;通过本次课程设计&#xff0c;巩固所学 Java 知识&#xff0c;了解 Java 项目的开发流程。本程序是使用 Java 开发的一款学生管理系统&#xff0c;设计中使用…

微信开放小程序SDK,几款SDK产品对比分析

前言 这几天看到微信团队推出了一个名为 Donut 的小程序原生语法开发移动应用框架&#xff0c;通俗的讲就是将微信小程序的能力开放给其他的企业&#xff0c;第三方的 App 也能像微信一样运行小程序了。 其实不止微信&#xff0c;面对广阔的B端市场&#xff0c;阿里也早已开放…