C++二叉搜索树BinarySearchTree

news2025/2/22 19:43:32

一、介绍

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

1.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值

2.若它的右子树不为空,则右子树上所有节点的值都大于根节点的值

3.它的左右子树也分别为二叉搜索树

因此,在二叉搜索树中,每个值都有对应且唯一的位置。

二、二叉搜索树的模拟实现

1.Insert 插入

插入一个数据,根据二叉搜索树的要求,需要找到合适的位置,并且进行链接

首先是找到要插入的位置,用一个cur遍历,当插入的值cur小走左边,比cur大则走右边,找到对应位置后,我们还需要链接,因此还需要一个父节点,可以定义一个parent一起走,找到后定义一个新节点插入数据,进行链接即可。

		//插入数据
		bool Insert(const K& key)
		{
			node* cur = _root;
			node* parent = nullptr;

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

2.InOrder 中序遍历

实现了数据的插入以后,为了验证实现的有没有bug,可以先将中序遍历给实现,根据二叉搜索树的特性,中序变量刚好是升序排序,将数据乱序插入后,中序打印,观察是否完成排序

中序遍历的实现是通过递归方式,在类里面实现递归需要用到_root作为参数,但外部接口一般不允许其获得内部成员变量,哪怕是提供一个函数接口获取,使用上也比较麻烦,因此可以在实现的时候再嵌套一层函数,在内部传参

		//中序遍历
		void InOrder()
		{
			_InOrder(_root);
			cout << endl;
		}
		void _InOrder(const node* root)
		{
			if (root == nullptr)
			{
				return;
			}
			_InOrder(root->_left);
			cout << root->_key << " ";
			_InOrder(root->_right);
		}

3.Find 查找

查找的实现非常简单,只需要cur变量去按照二叉搜索树的特性去遍历即可,若是没找到则返回空指针,找到返回节点地址

		node* Find(const K& key)
		{
			node* cur = _root;
			while (cur)
			{
				if (key < cur->_key)
				{
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
					cur = cur->_right;
				}
				else
				{
					return cur;
				}
			}
			return nullptr;
		}

4.Erase 删除

删除是本章模拟实现部分的重点,也是最为复杂的,要分类讨论被删除对象的各种情况,条件判断上较为复杂

1.被删除的节点是叶子节点(既没有左孩子,也没有右孩子),则直接删除即可

2.被删除的节点有一个孩子节点,则此时需要将孩子进行托孤(将孩子节点与被删除的节点的父节点进行链接),此时需要分类讨论,当被删除节点是其父节点的左孩子时,则托孤给父节点的左指针,被删除节点是右孩子时,则托孤给右节点

观察发现,其实情况1和情况2可以被并为一类,只有一个孩子或者没有孩子时,都按照托孤的思路执行,并且由于需要链接,因此在往下找被删除节点时,会同时记录它的父节点,父节点需要根据情况分类讨论用哪个指针继承孩子,并且还有考虑到一种特殊情况,就是删除根时,要特殊处理

3.当被删除的节点有两个孩子时,则需要用替代法,即找被删除节点左边最大的值,或者右边最小的值与被删除的值进行替换,然后再将用于替换位置的节点删掉

注意,左边最大值,就是从左子树的根开始,一直往右找,右边最小值,就从右子树的根开始,一直往左找,因此,拿右边最小值举例,右边最小值一定没有左孩子,而可能有右孩子需要链接,右孩子的链接同样需要分类讨论

参考代码:

		//删除
		bool Erase(const K& key)
		{
			node* cur = _root;
			node* parent = nullptr;
			while (cur)
			{
				if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else//找到要删除的节点了
				{
					if (cur->_left == nullptr || cur->_right == nullptr)//托孤
					{
						if (cur == _root)//被删的为根时,需要更新根
						{
							if (cur->_left != nullptr)
							{
								_root = cur->_left;
							}
							else
							{
								_root = cur->_right;
							}
						}
						else//分情况链接
						{
							if (parent->_left == cur)//要删除的节点在父节点的左边,则无论如何都是链接左边
							{
								if (cur->_left != nullptr)
								{
									parent->_left = cur->_left;
								}
								else
								{
									parent->_left = cur->_right;
								}
							}
							else if (parent->_right == cur)//要删除的节点在父节点的左边,则无论如何都是链接左边
							{
								if (cur->_left != nullptr)
								{
									parent->_right = cur->_left;
								}
								else
								{
									parent->_right = cur->_right;
								}
							}
						}
						delete cur;
						return true;
					}
					else//要删除的节点有两个孩子
					{
						node* min_right = cur->_right;
						node* pmin_right = cur;
						while (min_right->_left)
						{
							pmin_right = min_right;
							min_right = min_right->_left;
						}
						cur->_key = min_right->_key;
						if (pmin_right->_left == min_right)
						{
							pmin_right->_left = min_right->_right;
						}
						else
						{
							pmin_right->_right = min_right->_right;
						}
						delete min_right;
						return true;
					}

				}
			
			}
			//找不到要删除的对象
			return false;
		}

三、递归实现接口

递归实现在思路上相对没那么直接,但是在代码上会简洁很多,当然在类里面递归都需要封装一层

1.Finer

递归实现查找很简单,遇到空则说明没找到,则返回nullptr,值比根小则往左子树找,值比根大则在右子树找,找到返回地址。

		node* Findr(const K& key)
		{
			return _Findr(_root, key);
		}
		node* _Findr(node* root, const K& key)
		{
			if (root == nullptr)
			{
				return nullptr;
			}

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

2.Insertr

在插入数据中,有个很巧妙的对引用的运用,就是在传root指针时,传引用,则往下递归的root就是上一层指针的别名,因此在找到需要插入的位置时,不需要多记录一个父节点去进行链接,而是此时的root就是上一层父节点的指针的别名,因此可以直接new一个节点进行链接

		bool Insertr(const K& key)
		{
			return _Insertr(_root, key);
		}
		bool _Insertr(node*& root, const K& key)
		{
			if (root == nullptr)
			{
				root = new node(key);
				return true;
			}

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

3.Eraser

删除的思路和非递归的一样,递归部分主要可以利用root指针传引用的巧妙之处去省下很多处理

参考代码:

		bool Eraser(const K& key)
		{
			return _Eraser(_root, key);
		}
		bool _Eraser(node*& root, const K& key)
		{
			if (root == nullptr)
				return false;

			if (key < root->_key)
			{
				return _Eraser(root->_left, key);
			}
			else if (key > root->_key)
			{
				return _Eraser(root->_right, key);
			}
			else
			{
				node* del = root;
				if (del->_left == nullptr)
				{
					root = root->_right;
				}
				else if (del->_right == nullptr)
				{
					root = root->_left;
				}
				else
				{
					node* min_right = del->_right;
					while (min_right->_left)
					{
						min_right = min_right->_left;
					}
					root->_key = min_right->_key;
					return _Eraser(root->_right, min_right->_key);
				}
				delete del;
				return true;
			}
		}

四、构造与析构

1.构造函数

默认的构造即可,写一份出来是为了写拷贝构造时也能调默认的构造

		BSTree()
			:_root(nullptr)
		{}

2.拷贝构造

拷贝构造只需要递归遍历,前序遍历拷贝节点即可,可以用引用传root

		BSTree(const BSTree& n)
			:_root(nullptr)
		{
			copy(_root, n._root);
		}
		void copy(node*& root,const node* n)
		{
			if (n == nullptr)
				return;
			root = new node(n->_key);
			copy(root->_left, n->_left);
			copy(root->_right, n->_right);
		}

3.赋值重载operator=

		BSTree& operator=(BSTree tmp)
		{
			swap(_root, tmp._root);
			return *this;
		}

4.析构函数

		~BSTree()
		{
			Destroy(_root);
		}
		void Destroy(node*& root)
		{
			if (root == nullptr)
				return;
			Destroy(root->_left);
			Destroy(root->_right);
			delete root;
			root = nullptr;
		}

五、二叉搜索树的应用场景

1. K模型

K模型即只有key作为关键字,结构中只需要存储Key即可,关键字即为需要搜索到的值。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下: 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树,在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

K模型针对的是“在不在”的问题

2.KV模型

KV模型比起K模型多了一个关键字val,每一个关键码key,都有与之对应的值Value,即<Key,Value>的键值对。

比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文就构成一种键值对;

再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是就构成一种键值对;

3.实际场景应用

对应K模型的底层,就是最初我们实现的那一个版本,只需要稍微修改一下,就可以改成KV模型

在输入部分(构造,拷贝构造,节点定义,插入等等),多加一个参数value,基本逻辑都是用key去操作,因此不需要做大变动

例一:中英互译

void TestBSTree3()
{
 // 输入单词,查找单词对应的中文翻译
 BSTree<string, string> dict;
 dict.Insert("string", "字符串");
 dict.Insert("tree", "树");
 dict.Insert("left", "左边、剩余");
 dict.Insert("right", "右边");
 dict.Insert("sort", "排序");
 // 插入词库中所有单词
 string str;
 while (cin>>str)
 {
 BSTreeNode<string, string>* ret = dict.Find(str);
 if (ret == nullptr)
 {
 cout << "单词拼写错误,词库中没有这个单词:" <<str <<endl;
 }
 else
 {
 cout << str << "中文翻译:" << ret->_value << endl;
 }
 }
}

例二:水果统计

void TestBSTree4()
{
 // 统计水果出现的次数
 string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", 
"苹果", "香蕉", "苹果", "香蕉" };
 BSTree<string, int> countTree;
 for (const auto& str : arr)
 {
 // 先查找水果在不在搜索树中
 // 1、不在,说明水果第一次出现,则插入<水果, 1>
 // 2、在,则查找到的节点中水果对应的次数++
 //BSTreeNode<string, int>* ret = countTree.Find(str);
 auto ret = countTree.Find(str);
 if (ret == NULL)
 {
 countTree.Insert(str, 1);
 }
 else
 {
 ret->_value++;
 }
 }
 countTree.InOrder();
}

六、二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二 叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:O(log2 N)

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:O(N)

因此后续会继续学习平衡搜索二叉树:AVL树和红黑树,解决上面歪脖子的问题

总结

本章介绍了搜索二叉树的特性,并且用C++模拟实现,详细分析了代码思路

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

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

相关文章

C#,数值计算——积分方程与逆理论,构造n点等间隔求积的权重的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// 构造n点等间隔求积的权重 /// Constructs weights for the n-point equal-interval quadrature /// from O to(n-1)h of a function f(x) times an arbitrary /// (pos…

Parasoft C/C++test:汽车网络安全ISO 21434最佳实践

为什么汽车网络安全很重要Why Automotive Cybersecurity Is Important 许多汽车公司向电子道路车辆的转变从根本上改变了整个行业&#xff0c;提高了汽车的互联性和智能性。随着电子汽车变得更加互联和智能&#xff0c;它们也越来越依赖软件来实现车辆操作&#xff0c;驱动更多…

Netty实战专栏 | JavaIO演进之路

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; Netty实战专栏 ✨特色专栏&#xff1a…

性能优于BERT的FLAIR:一篇文章入门Flair模型

文章目录 What is FLAIR&#xff1f;FLAIR ModelContextual String Embedding for Sequence Labelingexample FLAIR Application AreaSentiment AnalysisNamed Entity RecognitionText Classification FLAIR一、什么是FLAIR&#xff1f;二、FLAIR Library的优势是什么&#xff…

Linux flock和fcntl函数详解

文章目录 flock函数描述返回值和错误码笔记 fcntl函数描述复制文件描述符文件描述标志文件状态标志 咨询锁强制锁管理信号租赁文件和目录变更通知改变管道容量 返回值错误备注遗留问题 flock函数 主要功能是在已打开的文件应用或者删除共享锁或者独占锁。sys/file.h声明了这个…

时间序列聚类的直观方法

一、介绍 我们将使用轮廓分数和一些距离度量来执行时间序列聚类实验&#xff0c;同时利用直观的可视化&#xff0c;让我们看看下面的时间序列&#xff1a; 这些可以被视为具有正弦、余弦、方波和锯齿波的四种不同的周期性时间序列 如果我们添加随机噪声和距原点的距离来沿 y 轴…

苹果加大对印度的扶持,提高在其生产iphone的比重

KlipC报道&#xff1a;跟踪苹果产业链&#xff0c;有分析师预计2023年全球约12%-14%的iphone在印度生产&#xff0c;预计2024年&#xff0c;印度将生产20%-25%的iphone。 KlipC的合伙人Andi D表示&#xff1a;“近年来随着苹果对中国的以来&#xff0c;印度已经成为高科技制造和…

Netty实战专栏 | BIO详解

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; Netty实战专栏 ✨特色专栏&#xff1a…

ESP32 for Arduino 分区信息

忘记过去&#xff0c;超越自己 ❤️ 博客主页 单片机菜鸟哥&#xff0c;一个野生非专业硬件IOT爱好者 ❤️❤️ 本篇创建记录 2023-11-04❤️❤️ 本篇更新记录 2023-11-04❤️&#x1f389; 欢迎关注 &#x1f50e;点赞 &#x1f44d;收藏 ⭐️留言&#x1f4dd;&#x1f64f;…

“线性函数”和“非线性函数”是什么?

总是会把“线性函数”和“非线性函数”与“连续的数据类型”与“非连续的数据类型”混淆&#xff0c;特此记录 一、线性函数&#xff1a; 一个函数 f 是线性的&#xff0c;如果对于任何两个输入 x1​ 和 x2​ 和任何两个常数 a 和 b&#xff0c;下列等式成立&#xff1a; 例如…

BIOS开发笔记 - HDA Audio

在PC中,音频输出是一个重要的功能之一,目前大多数采用的是英特尔高清晰音效(英语:Intel High Definition Audio,简称为HD Audio或IHD)方案,它是由Intel于2004年所提出的音效技术,能够展现高清晰度的音质效果,且能进行多声道的播放,在音质(音效质量)上超越过去的其他…

Ubuntu18.04 下PCL的卸载与安装

目录 一、卸载有问题的PCL1.7 二、编译&&安装PCL1.8.1 2.1、安装PCL依赖 2.2、编译VTK 2.3、编译PCL源码 三、 总结 写这篇博客时&#xff0c;本文方法已经在笔记本Ubuntu和VM虚拟机成功安装PCL1.8.1&#xff0c;并且通过测试。 下文方法同样适用于ubuntu18.04。…

JsonPath 数据快速查找和提取工具

常用语法 表达式说明$表示根元素$.key选择根元素下的指定键名的值$.*选择根元素下的所有属性值$.array[*]选择根元素中的数组的所有元素$.key[subkey]选择根元素中的键名为key&#xff0c;子键名为subkey的值$.key[*].subkey选择根元素中的键名为key的所有元素的子键名为subke…

【PID专题】MATLAB如何实现PID?

MATLAB是一种非常强大的工具&#xff0c;用于实现和分析PID&#xff08;比例-积分-微分&#xff09;控制器。在MATLAB中&#xff0c;您可以使用控制系统工具箱来设计、模拟和调整PID控制系统。 以下是一般步骤&#xff0c;演示如何在MATLAB中实现PID控制&#xff1a; 1. 打开MA…

轻量封装WebGPU渲染系统示例<13>- 屏幕空间后处理效果(源码)

当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/main/src/voxgpu/sample/ScreenPostEffect.ts 此示例渲染系统实现的特性: 1. 用户态与系统态隔离。 细节请见&#xff1a;引擎系统设计思路 - 用户态与系统态隔离-CSDN博客 2. 高频调用与低频调用隔离。…

算法随想录算法训练营第四十七天| 647. 回文子串 516.最长回文子序列

647. 回文子串 题目&#xff1a;给你一个字符串 s &#xff0c;请你统计并返回这个字符串中 回文子串 的数目。回文字符串 是正着读和倒过来读一样的字符串。子字符串 是字符串中的由连续字符组成的一个序列。具有不同开始位置或结束位置的子串&#xff0c;即使是由相同的字…

Mysql高级——Mysql8一主一从,多主多从搭建

修改 /etc/hosts文件 ip地址 master1 ip地址 master2 ip地址 slave1 ip地址 slave2一主一从 create database master1db;create table master1db.master1tab(name char(50));insert into master1db.master1tab VALUES(1111);insert into master1db.master1tab VALUES(2222);m…

Opencascad(C++)-创建自定义坐标系

文章目录 1、前言2、在Opencascad中显示小的坐标系3、在Opencascad中创建自定义的坐标系 1、前言 在Opencascad开发时&#xff0c;在view中可以显示小的坐标系&#xff0c;但是有时我们需要在建模时创建基准坐标系&#xff0c;当然可以作为工件坐标系也可以作为基准坐标系。本…

2023面试知识点三

1、强软弱虚引用 强引用 当内存不足的时候&#xff0c;JVM开始垃圾回收&#xff0c;对于强引用的对象&#xff0c;就算是出现了OOM也不会对该对象进行回收&#xff0c;打死也不回收~&#xff01; 强引用是我们最常见的普通对象引用&#xff0c;只要还有一个强引用指向一个对象…

基于单片机的衣物消毒清洗机系统设计

收藏和点赞&#xff0c;您的关注是我创作的动力 文章目录 概要 一、系统总体设计2.2 功能分析2.3 系统框架设计 二、硬件电路设计3.1 电源模块的设计 三、 软件设计4.1 系统整体流程4.4 软件整体流程实物图 四、 结论五、 文章目录 概要 基于单片机的衣物消毒清洗机可以应用在…