[C++]普通二叉搜索树实现

news2024/11/26 8:43:12

目录

1 二叉搜索树的基本概念

2 二叉搜索树的构建

2.1 二叉搜索树的结点

2.2 搜索树类的结构

3 成员函数

3.1 插入

3.2 查找

3.3 删除(重点)

3.4 默认成员函数的辅助函数

4 普通的二叉搜索树的效率


1 二叉搜索树的基本概念

        二叉搜索树又称二叉排序树,它或者是一棵空树,他都有以下的特征:

1. 若是它的左子树不为空,那么左子树的所有节点值都小于根节点的值。

2. 若是它的右子树不为空,那么右子树的所有节点值都大于根节点的值。

3. 它的左右子树也符合二叉搜索树的特征。

        下图则是二叉搜索树的形象化展示:

         如图可以直观的感受到,当一棵树为二叉搜索树的时候,它的结构特征都满足我之前所描述的概念,并且,通过我箭头的访问方式,也可以看到,最后它的访问结果一定是一个升序的序列,这也是为什么二叉搜索树相比较于普通的二叉树来书,具有了实际的意义。

        所以在此基础上,我们才能对这个树的结点进行有意义的比较,插入,删除等操作。

2 二叉搜索树的构建

2.1 二叉搜索树的结点

template<class K>
struct BinaryNode
{
    K _key;
    BinaryNode<K>* _left;
    BinaryNode<K>* _right;

    BinaryNode<K>(const K& key)
        :_key(key),_left(nullptr),_right(nullptr){}
};

        作为一个二叉树,那么它必然的就需要有自己的结点,在这里博主采用了最简单的二叉链的方式为大家展示,也就是普通的数据、左节点、右节点,加上一个有参构造。

        对于博主而言,我是不太支持对于搜索树添加一个无参构造的方式,因为作为二叉搜索树,其每一个结点都是有自己独立的意义的,如果添加无参构造,那么生成的结点对于我们来说有什么实际的意义呢?不如直接对外不开放这功能,让用户感受到他编写时,实际的问题所在。

        然后又因为我们要进行范式编程,那么必然的,就需要用到模板相关的知识,而对于普通的二叉树结构而言,需要的无非就是存储的数据,或则是它的比较方式需要添加模板。例如有部分的数据是一个结构,像是pair之类的,但是博主这里默认他就是一个普通的单个变量,毕竟才刚开始,博主并不打算增加你们的负担。

2.2 搜索树类的结构

template<class K>
class BSTree
{
    typedef BinaryNode<K> Node;
public:
    BSTree()
    {

        _root = nullptr;

    }

     BSTree(const BSTree<K>& copy)
    {
        _root = _copy(copy);
    }

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

    ~BSTree()
    {
        destroy(_root);
    }

private:
    Node* _root;
};

        对于搜索树的结构这部分博主并不打算作解释,里面就是关于它的默认函数以及有一个根节点指针所组成,也没有必要讲解,默认成员函数里面有一些实际的功能,博主打算在下一节为大家讲解。

3 成员函数

3.1 插入

//插入
	bool insert(const K& key)
	{
		//第一次插入
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}

		//之后的插入,要保证之后的连接
		Node* cur = _root;

		while (cur)
		{
			//大于往右走
			if (key > cur->_key)
			{
				if (cur->_right == nullptr)
				{
					cur->_right = new Node(key);
					return true;
				}
				cur = cur->_right;
			}
			//小于往左走
			else if (key < cur->_key)
			{
				if (cur->_left == nullptr)
				{
					cur->_left = new Node(key);
					return true;
				}
				cur = cur->_left;
			}
			//已经有了该数据,插入失败
			else
			{
				return false;
			}
		}

		//不可能走到的位置
		return false;
	}

        普通的二叉搜索树的插入十分的简单,因为它的特性保证了,我们插入时的高效性,如下:

         如果我们的二叉树是一个链式结构,那么势必会让我们的每一次插入变为O(N)的时间复杂度,但是由于它是搜索树,根据搜索树的特性,在理想情况下,我们每一次比较都能够排除一半的选项,这也就代表了,插入的效率变为了O(log2_N)了,当然一般是没有这么好的结构的,我们之后再解释。

        回过头看代码,首先我们得保证时候为第一次插入,也就是开始时,我们搜索树对象的根在初始时为空,我们需要修改它,保证它的有效性,否则在之后访问根结点时会出现解引用根节点导致程序崩溃的问题。

        之后就是找插入位置的问题了,通过循环比较当前结点与插入结点key值的关系,然后再考虑向左移动还是向右移动,当检测到下一步移动的结点为空就表示了,找到了需要插入的位置,通过new结点的方式在这个位置连接上去。

        可能之前就有朋友会问了,如果插入了值我们已经有了应该怎么办呢?很明显,在我们的这个树结构里面没有很好的方式去解决它,不过也不是不能解决,学习库容器multimap也能够解决,也就是在这个结点的位置规定它的左节点,或则是右节点的位置像是链表的方式一样插入就好,其余的结构不作改变。博主并不想这么解决,所以所幸就直接返回一个插入失败就行了,简单粗暴。

3.2 查找

//查询
	bool find(const K& key) const
	{
		//树内还没有数据
		if (_root == nullptr)
		{
			return false;
		}

		//查找
		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;
	}

        对于查找就更简单了,博主也不想作解释,与插入方式基本一致。

3.3 删除(重点)

问题:

        看了上面的两段代码,我相信大家心中肯定会认为“这就是搜索树?感觉也没什么难度吗,我上我也行。”如果大家真是这么想的,那只能是大家小看它了,请大家自己思考以下,如果我们要删除一个结点应该怎么删除呢?

        我给大家一点提示,删除叶子结点,删除只有一个孩子的结点,删除有两个孩子的结点,删除根节点,总的也就是这几种情况,看看大家能不能想到办法解决这个问题呢?

         看到上面的这一棵搜索树,比如说我要删除42号结点、45号结点、15号结点、30号结点。

讲解:

        我们先从简单的问题开始入手,删除一个叶子结点应该是怎么样的呢?

        首先,叶子结点就是一个没有孩子的结点,向上也只有父节点连接这它,那么它的删除,只会影响到谁?那就是它的父亲的子节点,与其余的结点有关系吗?没有,那么删除之后就会变为下图:

         很轻松,也没有任何的问题,那么开始下一个问题,删除45应该怎么办?

        删除45也就是删除一个有一个孩子的结点,删除它本身并不重要,重要的是,我们如何让它的孩子还在我们的这个结构当中呢?这个时候我们就需要找一个人来帮忙管理了,谁有资格?当然是父亲结点,如果你在父亲的左边,那么连带你和你的子节点都会比父节点小,反之则是大。

        还有在连接的时候,需要判断是哪一个结点需要需要被管理,是父节点的那边代为管理。那么最终就会为我们呈现出如下的图:对结构也是没有任何影响的。

         最后就是删除15或则是30位置的结点应该怎么办呢?我们的父节点有没有能力帮助我们管理好两个孩子。所以这个时候就需要去聘请一个保姆咯,那么什么样的人可以作为我们的保姆呢?我也不买关子了,很简单,保姆只能是左子树最大的那个结点,或则是右子树最小的那个结点,一旦满足了这个条件,那么对应的,保姆就只能是只有一个孩子或则是没有孩子,我们对于没有孩子和有一个孩子的结点删除有方法吗?有!刚刚才讲嘛。

        那么为什么用左最大或则是右最小结点就能够当保姆了呢?请观察下图:

         如上图所示,我们将这两个结点的数据去覆盖原来删除位置的数据,是不是表示我们已经删除了这个结点了?毕竟结点本身没有意义,有意义的是他的数据,还有,我们替换了之后,他还是不是二叉搜索树了?是的,那么新的删除位置我们有能力直接删吗?有能力。那么为什么能这么做呢?因为左子树的最大结点作根一定满足小于右子树,大于左子树,同理右子树的最小结点也是一样的。

        所以这样的方式,就能够让我们实现对于一个有两个孩子的结点的删除,并且因为两个孩子的逻辑需要重复包含删除一个结点和删除两个结点,那么书写顺序就让其写在最前面,如果是拆分功能成为函数,则不需要这么考虑,直接调用即可。

代码:

//删除
bool erase(const K& key)
{
	//为空不能删除
	if (_root == nullptr)
	{
		return false;
	}

	//找到需要删除的那个结点位置
	//保留父节点
	Node* prev = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (key > cur->_key)
		{
			prev = cur;
			cur = cur->_right;
		}
		else if (key < cur->_key)
		{
			prev = cur;
			cur = cur->_left;
		}
		else
		{
			break;
		}
	}
	//没有找到
	if (cur == nullptr) return false;

	//找到了需要分多种情况,没有孩子,有一个孩子,有两个孩子
	if (cur->_left != nullptr && cur->_right != nullptr)
	{
		//一定有左结点
		prev = cur;
		Node* sub = cur->_left;

		//找左子树的最大结点,在左子树的最右位置
		while (sub->_right != nullptr)
		{
			//找到的数据一定只有一个孩子或者是没有孩子
			prev = sub;
			sub = sub->_right;
		}
		//用值去覆盖,然后更换新的删除目标
		cur->_key = sub->_key;
		cur = sub;
	}

	//没有孩子
	if (cur->_left == nullptr && cur->_right == nullptr)
	{
		//需要去掉父节点的指向
		if (prev != nullptr)
		{
			if (prev->_left == cur) prev->_left = nullptr;
			else prev->_right = nullptr;
		}
		delete cur;
		return true;
	}
	//有一个孩子,需要用父节点来帮忙管理
	else if ((!cur->_left && cur->_right) || (cur->_left && !cur->_right))
	{
		//父节点为空,表示需要删除的位置是根节点,那么此时直接把子节点放上来就行
		if (prev == nullptr)
		{
			//更新根节点为它的不为空的那一个孩子
			_root = cur->_left == nullptr ? cur->_right : cur->_left;
		}
		//左节点不为空
		else if (cur->_left != nullptr)
		{
			//判断需要父节点的哪一个孩子结点去接收
			if (prev->_left = cur)
				prev->_left = cur->_left;
			else
				prev->_right = cur->_left;
		}
		//右节点不为空
		else
		{
			//判断该节点连接到父节点的哪一个位置
			if (prev->_left = cur)
				prev->_left = cur->_right;
			else
				prev->_right = cur->_right;
		}
	}
	delete cur;
	return true;
}

3.4 默认成员函数的辅助函数

 代码:(本身逻辑比较简单,博主不讲解)

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

	Node* newNode = new Node(root->_key);
	newNode->_left = _copy(root->_left);
	newNode->_right = copy(root->_right);

	return newNode;
}

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

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

	delete root;
	root = nullptr;
}

4 普通的二叉搜索树的效率

        相信大家也看到了,搜索二叉树的效率,相比于我们的链表来说是更优秀的,但是他不稳定,为什么?因为他有可能成为下方的这种情况:

        这是不是二叉搜索树?是,但是它的搜索效率是多少?O(N),这不扯淡吗,我费半天力气,写了个这完蛋玩意出来,普通的二叉搜索树,并不可靠,那么也就证明了容器map、set使用的底层结构并不是它,而是它的升级版,有些是AVL树,有些是红黑树,但是主流的写法都是红黑树。本篇文章,博主不打算对这两个数据结构进行讲解,将会在之后分享给大家。


        以上就是博主对于普通的搜索二叉树的全部理解了,希望能够帮助到大家。

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

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

相关文章

Java框架学习05(Spring事务详解)

1、什么是事务&#xff1f; 事务是逻辑上的一组操作&#xff0c;要么都执行&#xff0c;要么都不执行。 我们系统的每个业务方法可能包括了多个原子性的数据库操作&#xff0c;比如下面的 savePerson() 方法中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的&…

相亲交友app开发上线运营的整个流程是什么

一、相亲交友app开发基本流程 1、需求分析&#xff1a;需求分析是相亲交友app源码开发的第一步&#xff0c;也是最重要的一步。在需求分析阶段&#xff0c;可以了解客户对于系统的需求&#xff0c;确定系统功能实现的大致方向和功能。 2、系统架构&#xff1a;系统架构阶段就是…

这里有一份教你每天用领英获取20个询盘的免费课程,手慢无

于2023年3月22日&#xff0c;我们圆满完成了深圳宝安的外贸分享交流会&#xff0c;时隔两个月即将迎来我们的广州场。 在上次深圳会议&#xff0c;有幸邀请到江西省跨境电商协会会长莅临 给大家分享了&#xff1a; 如何帮助传统制造业从“0”开始做外贸、如何借助平台为企业…

浅谈霍尔电流传感器在电池柜监测中的应用

安科瑞 耿敏花 摘要&#xff1a;本文分析了霍尔电流传感器的工作原理&#xff0c;浅谈其在电池柜监测中的应用。 关键词&#xff1a;霍尔电流传感器 工作原理 充放电电流 电池柜 引言 大多数的工厂里&#xff0c;使用到的电池柜&#xff0c;它是将许多的新组装的电池一起…

不合格机器人工程讲师为何不分享成功的案例

不合格机器人工程讲师如何坦然面对失败 除了失败&#xff0c;更多的失败&#xff0c;也并非一无所获。 博客分享过&#xff0c;但是关注度&#xff08;浏览量&#xff09;不高&#xff0c;大部分成功案例都是学生/毕业生自身努力的结果&#xff0c;教育引导的作用小于他们自身…

中国品牌日:海尔智家向世界展示“中国”

品牌&#xff0c;在任何时候都是一个厚重的话题。什么是品牌&#xff1f;被咬掉一口的苹果、圆润张扬的对号、还有那个大大的黄色M&#xff0c;在诞生之初也不过是个商标。只是后来&#xff0c;它们跟智能手机、体育和快餐划上了等号&#xff0c;讲出了故事、收获了口碑&#x…

Android Framework——Binder 监控方案

作者&#xff1a;低性能JsonCodec 在 Android 应用开发中&#xff0c;Binder 可以说是使用最为普遍的 IPC 机制了。我们考虑监控 Binder 这一 IPC 机制&#xff0c;一般是出于以下两个目的&#xff1a; 卡顿优化&#xff1a;IPC 流程完整链路较长&#xff0c;且依赖于其他进程…

操作系统基础知识介绍之内存层次结构(一)

传统上&#xff0c;内存层次结构的设计者专注于优化平均内存访问时间&#xff0c;这由缓存访问时间、未命中率和未命中惩罚决定。 然而&#xff0c;最近&#xff0c;功率已成为主要考虑因素。 在高端微处理器中&#xff0c;可能有 60 MiB 或更多的片上高速缓存&#xff0c;并且…

并查集-- 一种路径压缩实现

并查集用于计算图连通分量。 比如回答这样的问题&#xff1a; 社交媒体中&#xff0c;用户A和用户B是否属于同一个圈子里的&#xff1f;一个城市到另一个城市是否是可达的&#xff1f; 并查集适用于并不需要计算出图上具体的路径&#xff0c;只需要计算是否连通。 public i…

JavaScript 链表

&#xff08;成功的唯一秘诀——坚持最终一分钟。——柏拉图&#xff09; 链表 众所周知&#xff0c;数组的查询比链表快&#xff0c;但插入比链表慢。 这是因为链表是一种动态的数据结构&#xff0c;不同于数组的是&#xff0c;链表分配内存空间的灵活性&#xff0c;它不会像…

解决车载U盘:USB设备未连接 问题

U盘是一种常用的便携式存储设备&#xff0c;用于存储和传输数据。在U盘上使用的文件系统类型决定了它可以支持的文件大小、安全性和其他特性。以下是几种常见的U盘文件系统类型&#xff1a; 1. FAT32:这是U盘上最常用的文件系统类型之一。FAT32文件系统支持的最大文件大小为4GB…

Revit楼板:建筑楼板和结构楼板区别和垫层生成

一、Revit中建筑楼板和结构楼板的区别 Revit中&#xff0c;在我们做项目时楼板是最常见的结构之一&#xff0c;几乎每次都需要使用它。分为建筑楼板和结构楼板&#xff0c;是不是有很多小伙伴就很好奇,为什么分为两种楼板&#xff0c;那么他们是什么时候使用的呢?之间又有何区…

从测试小白成功转型自动化测试,我是如何一步步掌握坚持下来的?

目录 学习自动化测试的初衷 克服困难&#xff0c;掌握自动化测试技能 自动化测试在日常工作中的应用 第一个自动化测试脚本的完成 自动化测试技能带来的机会和挑战 【自动化测试工程师学习路线】 学习自动化测试的初衷 作为一名测试新人&#xff0c;刚进入测试行业的时候…

工业视觉检测的8个技术优势

工业4.0时代&#xff0c;自动化生产线成为了这个时代的主旋律&#xff0c;而工业视觉检测技术也成为其中亮眼的表现&#xff0c;其机器视觉技术为设备提供了智慧的双眼&#xff0c;让自动化的脚步得以加速&#xff01; 在实际的生产应用中&#xff0c;视觉技术方案往往先被着手…

zed2i相机中imu内参的标定及外参标定

zed2i中imu内参的标定 参考&#xff1a; https://blog.csdn.net/weixin_42681311/article/details/126109617 https://blog.csdn.net/weixin_43135184/article/details/123444090 值得注意&#xff0c;imu内参的标定其实不是那么重要&#xff0c;大致上给一个值应该影响不大…

金字塔特征融合

金字塔的三种主要结构 FPN: Feature Pyramid Networks for Object Detection (CVPR 2017) PANet: Path Aggregation Network for Instance Segmentation (CVPR 2018) BiFPN: EfficientDet: Scalable and Efficient Object Detection (CVPR 2020) Deep High-Resolution Repre…

神奇哈哈镜-第14届蓝桥杯省赛Scratch初级组真题第3题

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第132讲。 神奇哈哈镜&#xff0c;本题是2023年5月7日举行的第14届蓝桥杯省赛Scratch图形化编程初级组真题第3题&#…

颜值经济崛起,伽蓝开启采购数字化之旅

今天&#xff0c;数字化转型已成为颠覆性力量&#xff0c;很多行业被裹挟其中&#xff0c;或主动或被动&#xff0c;美妆行业也不例外。 作为国内最大的化妆品企业之一的伽蓝&#xff0c;在过去的几年当中&#xff0c;一直是以 7% 到 10% 的速度快速增长&#xff0c;在此过程中…

计算机组成原理---第二章 习题详解版

(一&#xff09;课内习题 1. &#xff08;二&#xff09;课后练习 1.写出下列各整数的原码、反码和补码表示&#xff08;用8位二进制表示&#xff09;。其中MSB是最高位&#xff08;符号位&#xff09;&#xff0c;LSB是最低位。 &#xff08;1&#xff09;-35 &#…

DVWA之文件包含漏洞

文件包含漏洞原理 1、什么是文件包含 程序开发人员一般会把重复使用的函数写到单个文件中&#xff0c;需要使用某个函数时直接调用此文件&#xff0c;而无需再次编写&#xff0c;这中文件调用的过程一般被称为文件包含。 2、文件包含漏洞 程序开发人员一般希望代码更灵活&a…