【C++】二叉搜索树(概念、实现、应用)

news2024/11/24 11:06:23

前言:

    此前我们在C语言实现数据结构的时候学习过二叉树,但是那个时候我们没有深入学习二叉搜索树。本章重提二叉树并详解二叉搜索树有下面两个原因:

  • 1、为我们下一章学习set和map做准备;
  • 2、详解我们进阶一点的二叉树的面试OJ题,加强我们深入的理解。

目录

(一)二叉搜索树的概念

(二)二叉搜索树的模拟实现

 (1)结点的声明

(2)几个默认成员函数

(3)非递归版本的增删查改操作

 (4)递归版本的增删查改操作


(一)二叉搜索树的概念

二叉搜索树又称二叉排序树,它或者是一棵 空树 ,或者是具有以下性质的二叉树:
  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

图示:

也就是说一棵二叉搜索树的任一个根节点,它的左子树所有节点的值都是小于根节点的值的,它的右子树所有结点的值都是大于根节点的值的。

二叉搜索树查找的时间复杂度:

  • 根据二叉搜索树的性质
  • 我们大多数人认为其搜索的一个值的速度是为树的高度次
  • 树的高度次的话,很多人就会认为是log2N次
  • 但是事实并不是,正确得查找时间复杂度是〇(N)

只有当是满二叉树或者是完全二叉树时间复杂度才是〇(logN)!!

当出现单边树的情况时,就是〇(N)的情况。

如:

由于没有二叉搜索树的官方库,我们增删查改的实现需要我们自己来完成,见下面的模拟实现。

(二)二叉搜索树的模拟实现

 (1)结点的声明

template<class K>
	class BSTreeNode
	{
	public:
		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;

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

	};

(2)几个默认成员函数

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


		BSTree() = default;//强制生成默认构造


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

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

		~BSTree()
		{
			Destroy(_root);
			//_root = nullptr;
		}
protected:


		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;
		}


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

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

			delete root;
			root = nullptr;
		}

private:
		Node* _root = nullptr;
}

构造函数:

  • 这里我们可以采用传统的方法
  • 直接初始化成员变量
  • 也可以用C++11的语法default
  • 强制编译器自己生成构造函数

拷贝构造函数:

  • 这里我们用了递归的方式进行拷贝
  • 采用根 - 左 - 右 的前序遍历的递归方式对整个二叉树拷贝
  • 最后将跟结点返回

赋值重载:

  • 我们采用“现代写法”
  • 我们把原根节点拷贝给一个形参k
  • 然后交换this指针指向的跟结点和k
  • 最后返回this指针的解引用

析构函数:

  • 析构函数我们这里也是采用递归的方式进行一个一个结点析构
  • 同样的我们再嵌套一个子函数
  • 也是采用类似前序遍历的方法将整个二叉树释放掉

采用递归方式的缺点就是如果数的结点个数足够多的时候,就会有爆栈的风险!!

(3)非递归版本的增删查改操作

1、查找find



		bool Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key == key)
				{
					return true;
				}
				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				if (cur->_key > key)
				{
					cur = cur->_left;
				}
			}

			return false;
		}

根据二叉搜索树的性质,查找规则如下:

  • 从根节点找起
  • 如果比该结点指向的key小,往左子树查找
  • 如果比该结点指向的key大,往右子树查找
  • 在子树重复上述操作,最终找到寻找的值
  • 否则,返回false

所以再没有平衡二叉搜索树的情况下,查找的时间复杂度为〇(N)


2、插入insert

bool Insert(const K& key)
		{
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}
			Node* parent = nullptr;
			Node* cur = _root;

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

			cur = new Node(key);
			if (parent->_key < key)
			{
				parent->_right = cur;
			}
			if (parent->_key > key)
			{
				parent->_left = cur;
			}

			return true;
		}

根据二叉搜索树的性质,插入规则如下:

  • 插入的前提是找到要插入的位置
  • 参考find,我们先找到要插入的位置
  • 切记在查找位置时,要注意记录结点的父亲,以便插入

3、删除*(重点理解)

二叉搜索树结点的删除是一件非常麻烦的事情:

  • 要删除结点,就要理清楚父子节点的链接关系(一不留神就把关系理乱了)
  • 要求删过之后的二叉树还是一棵搜索二叉树(相当困难,普通直接删除做不到)
	bool Erase(const K& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;

				}
				else
				{
					if (cur->_left == nullptr)
					{

						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else if (parent->_right == cur)
							{
								parent->_right = cur->_right;
							}
						}
						delete cur;
					}

					else if (cur->_right == nullptr)
					{

						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else if (parent->_right == cur)
							{
								parent->_right = cur->_left;
							}
						}
						delete cur;
					}

					else
					{
						// 找右树最小节点替代,也可以是左树最大节点替代
						Node* pminRight = cur;
						Node* minRight = cur->_right;


						while (minRight->_left)
						{
							pminRight = minRight;
							minRight = minRight->_left;
						}
						cur->_key = minRight->_key;

						if (pminRight->_left == minRight)
						{
							pminRight->_left = minRight->_right;
						}
						else
						{
							pminRight->_right = minRight->_right;

						}

						delete minRight;

					}
					return true;
				}
			}

			return false;
		}

在讲解之前我们先分析一下不同位置结点删除的情况:

(1)当没有孩子或者只有一个孩子时

  • 可以直接删除,孩子托管给父亲 — (托孤)

如我们要删除7和14:

 此时我们就是将该结点直接删除,然后把该删除结点的孩子托给他的父亲。

以删除14这个结点为例:

  • 该结点比10这个结点(父结点)大,在其右子树
  • 那么该右子树的所有的值都比10这个结点大
  • 所以要链接在10这个结点的右边

以删除7这个结点为例:

  • 该结点比6这个结点(父结点)大,在其右子树
  • 因为7这个结点没有孩子
  • 直接删除,将父节点(6结点)的右指向空

(2)当有两个孩子时 

这个时候就没法托孤了,我们要想一个新的方法。

这里我们直接给出核心步骤:

  • 要找到 【左子树的最大值节点,或者右子树的最小值节点】
  • 找到之后,将要删除的结点和找到的结点的值进行交换(这里我们暂时用的是值交换)
  • 再将被交换过之后的值的结点删除
  • 一般被交换的结点都是末尾的叶子结点(按照上述的没有孩子的结点删除方式删除)

仔细思考为什么要这样做?

  • 首先,找到左子树最大结点或者右子树最小结点替代原来的相对的根结点位置,那么他的左右满足搜索二叉树的性质;
  • 其次,我们找到之后交换他和待删除的结点的值,左子树最大结点或者右子树最小结点肯定是叶子结点,交换后易删除,不会对结构有大的影响。

以删除3这个结点为例:

  • 先找到以3为根节点的右子树的最小的结点,然后交换结点的key
  • 然后参考上面的方法我们删除交换后3的结点然后“托孤”。

 (4)递归版本的增删查改操作

在分块讲解前,我们要明白,递归要传根节点,但是用户平常使用这些借口并不会传根节点,而是直接调用,所以我们嵌套一层,这样就可以满足双方的需求了。如下:

 1、查找FindR

我们把握好递归结束的条件然后左右递归即可。

bool _FindR(Node* root, const K& key)
		{
			if (root == nullptr)
			{
				return false;
			}
			if (root->_key == key)
			{
				return true;
			}

			if (root->_key < key)
				return _FindR(root->_right, key);

			if (root->_key > key)
				return _FindR(root->_left, key);
		}

2、插入InsertR

bool _InsertR(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				root = new Node(key);//这里用了引用返回时链接,画图
				return true;
			}

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

该如何链接上树呢?

可以在递归的参数中多一个父亲结点,每次递归都更新一下Parent,然后再带到下一层递归
显然这样在学过C++之后就麻烦了。
用了一个指针的引用就解决了问题

  • 因为root的值此时是空,但是root同时是这个结点里的_left这个指针的别名
  • 相当于当前结点的父节点的左指针的别名
  • 意味着此时再去给root赋值就是去给该结点父亲结点的_left赋值
  • 那么此时就链接起来了
     

3、删除EraseR


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

			if (root->_key < key)
				return _EraseR(root->_right, key);
			else if (root->_key > key)
				return _EraseR(root->_left, key);
			else
			{
				Node* del = root;

				if (root->_left == nullptr)
					root = root->_right;

				else if (root->_right == nullptr)
					root = root->_left;

				else
				{
					Node* maxleft = root->_left;
					while (maxleft->_right)
					{
						maxleft = maxleft->_right;
					}
					swap(root->_key, maxleft->_key);

					return _EraseR(root->_left, key);
				}
				delete del;
				return true;
			}
		}
  • 先查找待删除的结点
  • 相等时就开始删除了(递归只是用来查找要删除的数的位置)
  •  root是要删除结点的左结点 / 右结点的别名

分两种情况删除:

  • 要删除的结点左(或右)为空
  • 要删除的结点左右都为空(替换法)

参照非递归版本,思路是一样的。

递归这种思想我么你还需要多加理解,下一章将详细讲解OJ题目,加深我们对于递归的理解!
 

感谢您的阅读!!

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

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

相关文章

【C++学习】日积月累——二叉搜索树详解

一、二叉搜索树 1.1 二叉搜索树概念 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵树&#xff0c;或者是具有以下性质的二叉树&#xff1a; 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值&#xff1b;若它的右子树不为空&#xff0c;则右子树上…

程序员常用的代码比较工具,你更喜欢哪款?

目录 &#x1f4a1; Linux 命令行的对比工具 一. diff 二. vimdiff命令 &#x1f4a1; GUI 比对工具 三. WinMerge 四. Diffuse 五. Code Compare 六. Beyond Compare 七. UltraCompare 八. Altova DiffDog 九. Kompare 十. Meld 十一. XXdiff 十二. KDiff3 十…

拓扑排序模板及例题

概念 一个有向无环图必然存在一个拓扑序列与之对应。 流程&#xff1a; 先将所有入度为0的节点入队将队列中的节点出队&#xff0c;出队序列就是对应拓扑序。对于弹出的节点x&#xff0c;遍历x所有出度y&#xff0c;对y进行入读减一操作检查入度减一之后的节点y&#xff0c;…

慈航公益·高莞乡情行

2023年4月9日&#xff0c;慈航公益高莞乡情行---高村陈屋第二届“敬老睦宗情暖乡土”活动日暨“英华教育奖学基金”成立大会在高村陈屋如期举行。 高村陈屋位于河源市连平县境内&#xff0c;为了赶上10点开始的活动&#xff0c;清晨六点半&#xff0c;志愿者们便从慈航出发&am…

12电感的应用

目录 一、电源电路使用 1、设计实例 二、高频电路中使用 1、选择Q值高的电感器 2、选择自谐振频率高的电感器 3、选择电感偏差小的电感器 三、控制振荡频率 四、确保高频信号的隔离 五、共模轭流线圈 一、电源电路使用 如式&#xff08;5&#xff09;所示&#xff0c;…

数码照片管理系统Damselfly

什么是 Damselfly &#xff1f; Damselfly 是一个基于服务器的数码照片管理系统。Damselfly 旨在管理基于文件夹的大型照片集合&#xff0c;特别关注快速搜索和关键字标记工作流程。Damselfly 包含强大的机器学习功能&#xff0c;可帮助您识别照片及其主体&#xff0c;包括人脸…

使用Socks5代理保障Windows网络安全

摘要&#xff1a;Socks5代理是一种在Windows系统中保障网络安全的有效方法。本文将详细介绍什么是Socks5代理&#xff0c;以及如何在Windows系统中使用Socks5代理来加强网络安全。同时&#xff0c;我们还将探讨如何编写代码来使用Socks5代理来保障应用程序的网络安全。 正文&am…

报表VS分析:为什么报表做不完?老板到底想要什么?

各位数据的朋友&#xff0c;大家好&#xff0c;我是老周道数据&#xff0c;和你一起&#xff0c;用常人思维数据分析&#xff0c;通过数据讲故事。 上一讲和大家讲了分析模型中的战斗机——财务分析模型。通过奥威BI软件的行计算模型来开发财务分析报表异常地简单&#xff0c;…

安装Django

1. 在物理环境安装Django Python官方的PyPi仓库为我们提供了一个统一的代码托管仓库&#xff0c;所有的第三方库&#xff0c;甚至你自己写的开源模块&#xff0c;都可以发布到这里&#xff0c;让全世界的人分享下载 pip是最有名的Python包管理工具 。提供了对Python包的查找、…

Linux 动态库的制作与使用

目录 动态库的制作和使用 动态库的制作和使用 原始结构如下&#xff1a; 先进入calc文件&#xff0c;并生成与位置无关的.o文件 接着生成动态文件库&#xff0c;使用ll指令可以看到&#xff0c;库名为绿色&#xff0c;linux中绿色的文件一般都是可执行文件 将其生成的lib…

如何快速搭建一个SpringBoot项目

前面我们了解了SpringBoot背景和特点&#xff0c;本节我们主要介绍如何快速构建一个SpringBoot项目&#xff0c;以此来提升日常开发效率。 SpringBoot是搭建应用的手脚架&#xff0c;由Spring公司的核心团队在2013年开始研发、2014年4月发布第一个版本的全新开源的轻量级框架。…

如何平衡倾斜摄影的三维模型轻量化数据文件大小和质量效果?

如何平衡倾斜摄影的三维模型轻量化数据文件大小和质量效果&#xff1f; 倾斜摄影超大场景的三维模型数据文件大小的具体范围取决于多种因素&#xff0c;如原始数据的复杂度、轻量化处理的方式和压缩算法等。一般而言&#xff0c;经过轻量化处理后&#xff0c;数据文件大小可以减…

centos7安装nginx及uwsgi部署django项目

1、安装配置uwsgi pip install uwsgi 2、在项目根目录下创建image_ocr_uwsgi.ini配置文件 [uwsgi] # 对外提供http服务的端口 http :9000 # 用于和nginx进行数据交互的端口 socket 127.0.0.1:8001 # django程序的主目录 chdir /home/image_process/image_ocr/image_ocr #…

【计算机架构】响应时间和吞吐量 | 相对性能 | 计算 CPU 时间 | 指令技术与 CPI | T=CC/CR, CC=IC*CPI

目录 0x00 响应时间和吞吐量&#xff08;Response Time and Throughput&#xff09; 0x01 相对性能&#xff08;Relative Performance&#xff09; 0x02 执行时间测量&#xff08;Measuring Execution Time&#xff09; 0x03 CPU 时钟&#xff08;Clocking&#xff09; 0x…

【RabbitMQ】| 狮子带你(超详细)原生Java操作兔子队列

目录 一. &#x1f981; 前言二. &#x1f981; 原生Java操作RabbitMQⅠ. 简单模式1. 添加依赖2. 编写生产者3. 编写消费者 Ⅱ. 工作队列模式1. 编写生产者2. 编写消费者3. 实现 Ⅲ. 发布订阅模式1. 编写生产者2. 编写消费者 Ⅳ. 路由模式1. 编写生产者2. 编写消费者 Ⅴ. 通配符…

SpringCloud源码之Spring Cloud Common核心接口说明

spring cloud commons spring cloud提供的通用抽象包&#xff0c;组件的实现基本上都依赖于当前包的接口定义实现功能&#xff0c;下面就是梳理一下当前包中都提供了哪些比较重要的接口 1. 服务注册 1.1 DiscoveryClient DiscoveryClient 是一个顶级的接口类&#xff0c;用…

node项目的建立

文章目录 1.node项目的建立1.1项目初始化1.2 安装express1.3 初始化服务器 2.配置跨域2.1安装cors2.2cors的引入&#xff08;app.js中&#xff09; 3.初始化路由3.1新建文件3.2初始路由模块3.3app.js注册3.4 在postman测试 4.抽离路由处理模块3.1 在router_handler新建user.js3…

为什么LC谐振频率附近信号会被放大

这个是LC低通滤波电路&#xff0c; 它的增益曲线是这样的 很多同学不理解为什么谐振频率附近信号会被放大&#xff0c;今天就来聊一聊为什么谐振频率附近信号会被放大。 看到这个LC低通滤波电路&#xff0c;假设输入信号源内阻为Rs&#xff0c;L和C为理想电感和电容&#xff0…

Jmeter(五)_CSV Data参数化,Beanshell

一.CSV Data Set Config 准备好一个txt文件&#xff0c;写入如下内容&#xff0c;第一行可以不写&#xff0c;写了的话也会作为一组数据被运行&#xff1a; 然后把后缀名改为CSV&#xff0c;这样一个参数化文件就准备好了 然后打开jmeter&#xff0c;在需要使用这个参数化…

数据库系统-数据库查询实现算法

文章目录 一、一趟扫描算法1.1 算法概述1.2 算法逻辑&物理实现1.2.1 逻辑层面1.2.2 物理层面1.2.2.1 P11.2.2.2 P21.2.2.3 P31.2.2.4 P4 1.3 迭代器构造查询实现算法1.4 关系操作的一趟扫描算法1.4 基于索引的查询实现算法 二、两趟扫描算法2.1 两趟算法基本思想2.2 多路归…