【C++】二叉搜索树(概念、实现、应用以及OJ题详解)

news2024/12/23 7:33:55

前言:

    此前我们在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/455295.html

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

相关文章

120名顶级技术专家用GPT-4搞出的脑洞发明大赏

文 | 智商掉了一地 黑客松&#xff08;Hackathon&#xff09;是一种聚集程序员、设计师等技术人才&#xff0c;共同在短短几天的时间内合作进行软件开发、解决问题的活动。参与者可分为个人和团队形式参与&#xff0c;他们将利用这段时间内的集中创作和多学科合作&#xff0c;迅…

Java网络编程系列之NIO

Java网络编程系列之NIO 1.Java NIO概述1.1 阻塞IO1.2 非阻塞IO1.3 NIO概述1.3.1 Channels1.3.2 Buffer1.3.3 Selector 2.Java NIO(Channel)2.1Channel概述2.2 Channel实现2.3 FileChannel 介绍与示例2.4 FileChannel 操作详解2.4.1 打开FileChannel2.4.2 从FileChannel读取数据…

带你一步步实现代码开发平台——概述、实现模式、整体框架

概述 低代码开发平台是一种开发工具&#xff0c;它允许用户使用图形界面和少量编码来创建应用程序。这种平台的目的是加快应用程序开发速度&#xff0c;减少开发成本和技能门槛。目前&#xff0c;市场上有许多低代码开发平台可供选择&#xff0c;包括Microsoft Power Apps、Ou…

学系统集成项目管理工程师(中项)系列11a_沟通管理(上)

1. 基本概念 1.1. 构成 1.1.1. 接收者和发送者 1.1.1.1. 参与者既发送信息&#xff0c;又接收反馈&#xff0c;是一体的 1.1.2. 信息&#xff08;Message&#xff09; 1.1.2.1. 多个参与者之间需要分享的信息&#xff0c;表达思想和情感的组成物 1.1.2.2. 信息的存在方式…

虚拟化技术 — Libvirt 异构虚拟化管理组件

目录 文章目录 目录Libvirtlibvirt API 函数库libvirtd Daemon软件架构权限模式运行模式XML 格式 virsh CLI Libvirt QEMU-KVM 环境部署HostOS 配置优化&#xff08;可选的&#xff09;开启 KVM Nested 嵌套虚拟化安装 CentOS GNOME 图形界面安装 Libvirt QEMU-KVM Libvirt 的…

C语言ctype.h头文件中2类好用的库函数

本篇博客会讲解C语言ctype.h这个头文件中的2类好用的库函数&#xff0c;分别是字符分类函数和字符转换函数。 字符分类函数 字符分类函数&#xff0c;指的是判断一个字符是不是属于某个类别&#xff0c;如果属于这个类别&#xff0c;返回非0数&#xff1b;如果不属于这个类别…

性能测试工具 IxChariot:Tcl脚本调用方法介绍

ixChariot是一款功能强大的性能测试软件&#xff0c;可用来测试有线和无线性能&#xff0c;可以模拟真实应用程序流量&#xff0c;并提供关键性能指标&#xff0c;包括吞吐量、丢包、抖动、延迟、MOS等。本文简单介绍如何使用IxChariot Tcl API来实现自动化跑流。 目录 IxChari…

RK3399平台开发系列讲解(调试篇)断言的使用

🚀返回专栏总目录 文章目录 一、什么是断言二、静态断言三、运行时断言沉淀、分享、成长,让自己和他人都能有所收获!😄 📢断言为我们提供了一种可以静态或动态地检查程序在目标平台上整体状态的能力,与它相关的接口由头文件 assert.h 提供。 一、什么是断言 在编程中…

浏览器状态同步和路由-SSR和单页面应用的分析 【单页面应用和服务端渲染】

目录 单页面应用&#xff08;优缺点&#xff09;&#xff08;Single Page Application&#xff09; 优点&#xff1a; SPA的缺点&#xff1a; 服务端渲染&#xff08;Server Side Rendering&#xff09; SSR示例&#xff08;一个ssr小引擎&#xff09; SSR优缺点分析 总结…

Opencv+Python笔记(八)轮廓检测

目录 一、轮廓的检测和绘制1.读入图像2.将读入图像转化为灰度图3.对灰度图进行二值化 [图像的阈值化处理](https://blog.csdn.net/Ggs5s_/article/details/130301816?spm1001.2014.3001.5501)4.进行轮廓检测5.在原图中显示轮廓 二、轮廓层级关系1.RET_LIST2.RETR_EXTERNAL3. R…

座椅内饰如何「跟上」智能电动?这款智能概念座舱看到未来

进入智能电动汽车时代&#xff0c;理想的车内空间应该是怎样的&#xff1f;作为“内饰空间创造者”、全球三大汽车座椅及内饰厂商之一&#xff0c;丰田纺织在2023上海车展上给出了一系列解决方案。 4月19日&#xff0c;丰田纺织携诸多产品亮相本次上海车展&#xff0c;包括面向…

【速卖通】 AliExpress(速卖通)关键词搜索结果采集

采集场景 在AliExpress(速卖通) 首页中 http://www.aliexpress.com 中输入关键词&#xff0c;采集关键词搜索后得到的商品列表信息。 采集字段 关键词、标题、商品id、商品图片地址、商品详情链接、价格、免费退送货、星级、已出售数量、店铺名 采集结果 采集结果可导出为E…

C语言入门篇——函数篇

1、什么是函数 首先&#xff0c;什么是函数&#xff1f;函数(function)是完成特定任务的独立程序代码。单元语法规则定义了函数的结构和使用方式。虽然C中的函数和其他语言中的函数、子程序、过程作用相同&#xff0c;但是细节上略有不同。 为什么使用函数&#xff1f; 首先…

刷题训练2之AcWing第 96 场周赛

竞赛 - AcWing 一、完美数 4876. 完美数 - AcWing题库 1、题目 如果一个正整数能够被 2520 整除&#xff0c;则称该数为完美数。 给定一个正整数 n&#xff0c;请你计算 [1,n]范围内有多少个完美数。 输入格式 一个整数 n。 输出格式 一个整数&#xff0c;表示 [1,n] 范…

【社区图书馆】操作系统的经典书籍

操作系统的经典书籍 一、引言二、书籍的选择三、优缺点3.1、《操作系统》3.2、《计算机操作系统》 小结 一、引言 《操作系统》罗宇和《计算机操作系统》汤小丹这两本书都是关于操作系统的经典书籍&#xff0c;各有优势。 二、书籍的选择 首先&#xff0c;从内容深度上&…

倾斜摄影超大场景的三维模型的顶层合并常见的问题分析

倾斜摄影超大场景的三维模型的顶层合并常见的问题分析 倾斜摄影超大场景的三维模型顶层合并是将多个局部区域的点云或网格数据进行融合&#xff0c;生成一个整体的三维模型的过程。在这个过程中&#xff0c;常见的问题包括&#xff1a; 1、数据不一致。由于数据采集时间、空间…

SAP SM30表格维护生成器隐藏记录日志字段

1.背景 在表格维护生成器中往往会隐藏记录日志字段&#xff0c;不让用户直接查看&#xff0c;而供运维或者开发部门使用&#xff0c;如下所示&#xff1a; 2.实现 2.1 SM30逻辑流和屏幕元素中删除日志记录字段 2.2 创建事件&#xff0c;写入记录日志代码 2.2.1 记录日志方式…

Node.js使用CORS解决跨域问题的三种方法

目录 1、通过CORS中间键解决2、设置响应头3、app.all解决4、解决跨域问题案例 现如今&#xff0c;实现跨域数据请求&#xff0c;最主要的两种解决方案&#xff0c;分别是JSONP和CORS. JSONP:出现的早&#xff0c;兼容性好&#xff08;兼容低版本IE&#xff09;。是前端程序员为…

m1下利用dockerdesktop安装ELK

一、背景&#xff1a;公司有一个需求&#xff0c;就是将txt中的数据加载到es中&#xff0c;之前没用过es&#xff0c;想着先在本地安装一个&#xff0c;然后再做测试。 二、安装docker desktop 打开docker的官网&#xff0c;下载苹果芯片的docker 网址&#xff1a;https://ww…

当DevOps遇见AI,智能运维的黄金时代开启

文章目录 1. 当DevOps遇见AI&#xff0c;智能运维的黄金时代2. 什么是DevOpts&#xff1f;改变开发格局&#xff1a;测开、运开必然趋势3. 什么是Docker容器化&#xff0c;它会替代掉VM虚拟机吗&#xff1f;4. 运维的终点是开发5. 实际项目的部署案例6. 誉天程序员课程 1. 当De…