一篇文章讲透数据结构之二叉搜索树
前言
在前面的学习过程中,我们已经学习了二叉树的相关知识。在这里我们再使用C++来实现一些比较难的数据结构。
这篇文章用来实现二叉搜索树。
一.二叉搜索树
1.1二叉搜索树的定义
二叉搜索树(Binary Search Tree)是基于二叉树的一种升级版本,因为普通的二叉树没有实际应用的价值,无法进行插入、删除等操作,所以我们进行了升级,升级成了二叉搜索树。
二叉搜索树是一种特殊的二叉树,它对数据的存储有着极其严格的要求:左节点比根节点小,右节点比根节点大。
下面我们展示一下二叉树和搜索二叉树的区别:
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f53117d894b24058a8add35c30699293.png
下面,我们通过中序遍历来看一看遍历结果:
我们发现,我们构建出的树是有序的。
那么,如果我们通过中序遍历+二分查找的话,会发现查找的效率很高,为O(logN)。这也是二叉搜索树名字的由来。
1.2二叉搜索树的特点
二叉搜索树的特点就是:左小于根,右大于根。
- 若某个结点的左节点不为空,那么它一定小于它的父节点。
- 若某个结点的右节点不为空,那么它一定大于它的父节点。
- 中序遍历的结果是有序的,为升序。
下面我们就来实现一颗二叉搜索树
二.二叉搜索树的实现
2.1基本框架
和二叉树类似的是,我们在建立二叉搜索树时,需要两个结构体。
- 节点类:表示结点
- 树类:表示树,存储结点。
template <class K>
struct BinarySearchTreeNode
{BinarySearchTreeNode(const K& key):_left(nullptr),_right(nullptr),_key(key){}BinarySearchTreeNode<K>* _left;BinarySearchTreeNode<K>* _right;K _key;
};
template <class K>
class BinarySearchTree
{typedef BinarySearchTreeNode Node;
private:Node* _root=nullptr;
};
这样,我们便完成了大体的框架。
2.2 查找
由于我们的二叉搜索树是有序的。
因此,查找的逻辑如下:
- 若为空树,则返回false
- 查找值大于节点值,往右走。
- 查找值小于节点值,往左走。
- 相等时,则找到了结点。
因此,其实现如下:
bool Empty()const
{return _root = nullptr;
}
bool Find(const K& key)
{//空树if (Empty()){return false;}Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}elsereturn true;}
}
为了能够更好的理解这段代码,我给出如下图解:
2.3 插入
下面,我们来实现一下插入的算法。
对于插入而言,其实就是创建一个结点,然后查找到合适的位置并进行链接。
因此,我们可以想到,插入的大体逻辑如下:
- 先找到合适的位置
- 创建一个结点
- 将结点与其父节点进行链接
对于我们而言,找到合适的位置其实就是之前写的find函数。
因此,我们只需要copy一下代码,然后将新建结点并连接即可。
bool Insert(const K& key)
{//空的处理if (_root = nullptr){_root = new node(key);return true;}//查找//由于我们需要记录父结点,因此我们需要将parent记录出来。Node* parent = nullptr;Node* cur = _root;while (cur){//key大,往右走if (cur->_key < key){parent = cur;cur = cur->_right;}//key小,往左走else if (cur->_key > key){parent = cur;cur = cur->_left;}else{return false;}}//新建结点cur = new Node(key);//判断是父的左子节点or右子节点,连接。if (parent->_key > key)parent->_left = cur;elseparent->_right = cur;return true;
}
需要一提的是,我们现在做的二叉树是不允许冗余的,当两个数相同时,就寄!
看一段插入逻辑:
下面,我们看一段冗余的逻辑:
这时,就出现了冗余的情况,插入失败了。
2.4 删除
删除是搜索二叉树的重点内容,它需要我们考虑非常多的情况。
下面,我们介绍一下具体的删除逻辑:
- 先依照查找的逻辑,判断目标值存不存在
- 如果存在,则进行删除
- 如果不存在,则寄!
说起来是非常简单的,但是实际的删除逻辑是极其复杂的,因为情况有非常多种。
首先,我们先把第一步查找的逻辑复现出来,然后我们再对删除的情况进行具体的分析。
bool Erase(){//空的处理if (_root = nullptr){_root = new node(key);return true;}//查找//由于我们需要记录父结点,因此我们需要将parent记录出来。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{//具体的删除逻辑。}}}
下面,我们来思考删除逻辑的具体处理方式。
2.4.1右子树为空
我们对右子树为空的情况的处理:我们需要估计到其的子节点,因此我们需要将其父节点与其左子节点相连。
2.4.2左子树为空
对于左子树都为空而言,我们需要的的则是将其父节点与其右子节点相连。
2.4.3 左右子树都为空
删除的具体逻辑看图片
到现在为止,我们总结了三种情况,可以得出如下代码:
//具体的删除逻辑if (cur->_left == nullptr){if (parent->_left == cur) //是父的左{parent->_left = cur->_right;}else//是父的右parent->_right = cur->_right;}else if (cur->_right == nullptr){if (parent->_left == cur)//是父的左parent->_left = cur->_left;else//是父的右parent->_right = cur->_left;}
下面,我们来考虑一下左右子节点都为空的情况:
在左右子树都不为空的时候,因为cur有两个子节点,因此我们没有办法通过父节点与其子节点的链接解决问题了,这时我们应该怎么做呢?
我们这时的处理方式:
在不影响树的基本规则的情况下,找一个结点替代cur。
那么,现在问题就转化成为了如何找到那个能够取代cur的结点。
能够取代cur,那么就一定要满足二叉搜索树的限制关系。
我们假设在右子树,那么,这个结点一定要比父节点大,比左子节点大,比右子节点小。
这时,我们发现,cur的右子树的最左结点能够满足这点,也就是右子树的最小值。
代入上图:我们将3和6互换位置,得出如下结果:
同理,如果是左子树的话,就需要找到其最左结点,也就是左子树的最大值。
左子树的最大值和右子树的最小值我们选一个即可。
下面,我们来写写代码:
else//两个结点的情况
{Node* RrightMinParent == null;//下一段代码要修改这里Node* RightMin = cur->_right;while (leftMax->_left){RrightMinParent = RightMin;RightMin = RightMin->_left;}swap(cur->_key, RightMin->key);if (RrightMinParent->_left == RightMin){RrightMinParent->_left = RightMin->_left;//此时转换成为了删除没有子树的情况,等于左还是右都无所谓的}elseRrightMinParent->_right = RightMin->_left;
}
现在,我们已经完成了全部的逻辑,下面只需要我们进行一些边界处理即可以及删除掉结点即可。
请看代码注释:
bool Erase(){//空的处理if (_root = nullptr){_root = new node(key);return true;}//查找//由于我们需要记录父结点,因此我们需要将parent记录出来。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){//如果是根节点,则找不到parent,等于右结点即可。if (cur == _root){_root = _root->_right;}else{if (parent->_left == cur)parent->_left = cur->_right;elseparent->_right = cur->_right;}}else if (cur->_right == nullptr){//如果是根节点,则找不到parent,等于左结点即可。if (cur == _root){_root = _root->_left;}else{if (parent->_left == cur)parent->_left = cur->_left;elseparent->_right = cur->_left;}}else//两个结点的情况{RrightMinParent == cur;//下面的while循环可能进不去,此时若parent为空,寄!因此需初始化为curNode* RightMin = cur->_right;while (leftMax->_left){RrightMinParent = RightMin;RightMin = RightMin->_left;}swap(cur->_key, RightMin->key);if (RrightMinParent->_left == RightMin){RrightMinParent->_left = RightMin->_left;//此时转换成为了删除没有子树的情况,等于左还是右都无所谓的}elseRrightMinParent->_right = RightMin->_left;cur=RightMin;}delete cur;return true;}}return false;}
三.二叉搜索树的遍历
二叉搜索树的遍历和二叉树的遍历一模一样,在这里,我们只需要用到中序遍历
直接写在这里:
中序遍历:根->左->右
在使用CPP实现的二叉树中,我们有以下问题:
- 二叉树的根是私有属性,外界无法直接获取。
我们有如下的解决方案:
- 公有化(不安全)
- 通过函数获取(有点别扭,不爱写那么多)
- 封装封装再封装!劳资再来一层!(好用爱用)
我们采取解决方案3,解决方案3为:我们在private中实现中序遍历,然后在public里调用。
如下:
public:void InOrder(){return _Inorder(_root);}
private:void _Inorder(Node* root){if (_root = nullptr){return;}_Inorder(root->_left)cout<<root->_key<<endl;_Inorder(root->_right)}Node* _root=nullptr;
};
四.递归实现
4.1查找
有关查找的逻辑,我们也可以使用递归实现。
实现逻辑如下:
- 如果当前根小于key,则递归到右树查找
- 如果当前根大于key,则递归到左树查找
- 如果当前树为空,则返回false
- 若以上条件都不符合,则是找到了,返回true
public:bool FindR(){return _FindR()}
private:void _FindR(Node* root,const K& key){if (root == nullptr){return false;}if (root->_key > key){return _FindR( root->_left,key);}else if (root->_key < key){return _FindR(root->_right, key);}return true;}
4.2插入
使用递归查找的话很简单,找到地方了之后直接把值放进去即可。
public:bool _InsertR(const K& key){return _Insert(root, key);}
private:void _InsertR(Node*& root, const K& key)//&不是取地址,而是引用{if (root = nullptr){root = new Node(key);//因为传递了指针的引用,因此我们可以在这里new一个nodereturn true;}if (root->_key < key)return _Insert(root->_right,key);else if (root->_key > key)return _Insert(root->_left,key);elsereturn false;}
4.3递归删除
删除的递归逻辑也需要使用引用
使用引用的目的是,在不同的函数栈帧中可以删除掉同一个节点,而不是临时变量。
另外,我们在这里使用的删除逻辑还是非递归版的删除逻辑,只不过找到key的方式变为递归了,如下:
bool _EraseR(Node*& root, const K& key)
{if (root == nullptr) // 基本情况:当前结点为空,表示未找到key,删除失败。{return false;}if (root->_key < key)_EraseR(root->_right, key); // 在右子树中递归查找并删除,不需要返回值。else if (root->_key > key)_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* leftMax = root->_left; // 查找左子树中的最大结点while (leftMax->_right){leftMax = leftMax->_right;}swap(root->_key, leftMax->_key); // 交换当前结点和左子树最大结点的值return _EraseR(root->_left, key); // 递归删除左子树中的最大结点}delete del; // 删除当前结点return true; // 删除成功}
}
五.其他实现
5.1 销毁
我们通过后序遍历的思想来销毁,先递归左子树销毁,再递归右子树销毁。最后销毁根节点。
public:~BinarySearchTree(){Destory(_root);}
private:void Destory(Node*& root){if (root = nullptr)return;Destory(root->_left);Destory(root->_right);delete root;root = nullptr;}
5.2拷贝构造以及赋值重载
现在我们实现下拷贝构造来避免浅拷贝问题。
public:BinarySearchTree = default();BinarySearchTree(const BinarySearchTree<K>& a){_root = Copy(a._root);}BinarySearchTree<K>& operator=(BinarySearchTree<K> a){swap(_root, a._root);return *this;}
private:Node* Copy(Node* root){if (root = nullptr)return nullptr;Node* copyroot = new Node(root->_key);copyroot->_left = Copy(root->_left);copyroot->_right = Copy(root->_right);return copyroot;}
相关文章:
一篇文章讲透数据结构之二叉搜索树
前言 在前面的学习过程中,我们已经学习了二叉树的相关知识。在这里我们再使用C来实现一些比较难的数据结构。 这篇文章用来实现二叉搜索树。 一.二叉搜索树 1.1二叉搜索树的定义 二叉搜索树(Binary Search Tree)是基于二叉树的一种升级版…...
新手入门c++(8)
到时候了,是时候给你们讲一下其他的定义形式与格式化输入输出了。 1.长整型变量 长整型变量分为两种: ①long类型 在计算机编程中,long 类型是一个整型数据类型,用于存储较大的整数。它的大小和范围取决于操作系统和编译器的实…...
新手铲屎官提问,有哪几款噪音低的宠物空气净化器推荐
相信很多铲屎官都明白的的痛就是猫咪掉毛太严重,所以每次看到满天飞的浮毛时只想赶紧逃离,一点都不想清理。但是家是自己的,猫是自己的,健康也是自己的,不清理也得清理。 为了更有效的清理浮毛,我朋友特意…...
解决RabbitMQ脑裂问题
文章目录 前言一、现象二、解决办法 前言 RabbitMQ脑裂 一、现象 RabbitMQ镜像群出现脑裂现象,各个节点的MQ实例都“各自为政”,数据并不同步。 二、解决办法 # 停止mq sh rabbitmq-server stop_app # 查看mq进程是否存在 ps -ef | grep rabbitmq # …...
经纬恒润AUTOSAR成功适配芯钛科技Alioth TTA8车规级芯片
在汽车电子领域,功能安全扮演着守护者的角色,它确保了车辆在复杂多变的情况下保持稳定可靠的运行。随着汽车电子的复杂性增加,市场对产品功能安全的要求也日益提高。基于此背景,经纬恒润AUTOSAR基础软件产品INTEWORK-EAS-CP成功适…...
4、java random随机数、一维数组、二维数组
目录 Random类与随机数生成数组的概述与使用数组的内存分配与访问数组的常见问题与解决方案一维数组的遍历与操作二维数组的概述与遍历1. Random类与随机数生成 引言 在编程中,我们经常需要生成随机数,比如在游戏、模拟实验或者数据处理中。Java提供了一个非常方便的类Rand…...
C++ 魔法三钥:解锁高效编程的封装、继承与多态
快来参与讨论💬,点赞👍、收藏⭐、分享📤,共创活力社区。 目录 💯前言 💯封装 1.封装概念 2.封装格式 3.封装的原理 4.封装的作用 💯继承 1.继承的概念 2.继承格式 3.继承的…...
姿态传感器(学习笔记上)
上节我们学的是温湿传感器,这节我们学的是姿态传感器,虽然都是传感器,但是它们还是有很大的区别的,这节的传感器我们通过学习可知,开发板上的姿态传感器型号是QMI8658C,内部集成3轴加速度传感器和3轴陀螺仪…...
labelimg使用教程
快捷键 W:调出标注的十字架,开始标注 A:切换到上一张图片 D:切换到下一张图片 del:删除标注的矩形框 CtrlS:保存标注好的标签 Ctrl鼠标滚轮:按住Ctrl,然后滚动鼠标滚轮,…...
力扣21 : 合并两个有序链表
链表style 描述: 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例: 节点大小相同时,l1的节点在前 何解? 1,遍历两个链表,挨个比较节点大小 同时遍…...
【Spring】Spring Boot 配置文件(7)
本系列共涉及4个框架:Sping,SpringBoot,Spring MVC,Mybatis。 博客涉及框架的重要知识点,根据序号学习即可。 有什么不懂的都可以问我,看到消息会回复的,可能会不及时,请见谅!! 1、配置文件作…...
《向量数据库指南》——解锁Wikipedia文章向量的跨语言搜索秘籍
嘿,各位向量数据库和AI应用的小伙伴们,我是你们的老朋友王帅旭,大禹智库的向量数据库高级研究员,也是《向量数据库指南》的作者。今天,咱们来聊聊一个超棒的数据集——百万条 Wikipedia 文章向量,这可是我在研究过程中发现的一个宝藏啊! 首先,咱们得说说这个数据集的来…...
【力扣 + 牛客 | SQL题 | 每日5题】牛客SQL热题204,201,215
1. 力扣1126:查询活跃业务 1.1 题目: 事件表:Events ------------------------ | Column Name | Type | ------------------------ | business_id | int | | event_type | varchar | | occurrences | int | --------…...
下载数据集用于图像分类并自动分为训练集和测试集方法
一、背景 最近需要用Vision Transformer(ViT)完成图像分类任务,因此查到了WZMIAOMIAO的GitHub,里面有各种图像处理的方法。而图像处理的前期工作就是获取大量的数据集,用于训练模型参数,以准确识别或分类我…...
Python xlrd库介绍
一、简介 xlrd是一个用于读取Excel文件(.xls和.xlsx格式)的Python库。它提供了一系列函数来访问Excel文件中的数据,如读取工作表、单元格的值等。 二、安装 可以使用以下命令安装xlrd库: pip install xlrd 三、使用方法 1. 导入库: 示例…...
Javascript立即执行函数
//立即执行函数 把函数的声明看作一个整体声明结束就立即调用 // (function(){console.log(hello) // })(); console.log((function (){ return 0; })()); // let afunction(){ console.log(hello) }; console.log(typeof a);//function,数组:objeck...
Linux相关概念和易错知识点(17)(文件、文件的系统调用接口、C语言标准流)
目录 1.文件 (1)文件组成和访问 (2)文件的管理 (3)C语言标准流 (4)struct file ①文件操作表 ②文件内核缓冲区 (5)Linux下一切皆文件 (…...
三防加固工业平板国产化的现状与展望
在当今全球科技竞争日益激烈的背景下,工业4.0和智能制造的浪潮推动了工业自动化设备的迅速发展,其中,三防加固工业平板电脑作为连接物理世界与数字世界的桥梁,其重要性不言而喻。所谓“三防”,即防水、防尘、防震&…...
3.1.3 看对于“肮脏”页面的处理
3.1.3 看对于“肮脏”页面的处理 文章目录 3.1.3 看对于“肮脏”页面的处理再看对于“肮脏”页面的处理MmPageOutVirtualMemory() 再看对于“肮脏”页面的处理 MmPageOutVirtualMemory() NTSTATUS NTAPI MmPageOutVirtualMemory(PMADDRESS_SPACE AddressSpace,PMEMORY_AREA Me…...
学 Python 还是学 Java?——来自程序员的世纪困惑!
文章目录 1. Python:我就是简单,so what?2. Java:严谨到让你头疼,但大佬都在用!3. 到底谁更香?——关于学哪门语言的百思不得姐结论——到底该选谁?推荐阅读文章 每个程序员都可能面…...
Spring Web MVC 入门
1. 什么是 Spring Web MVC Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从从⼀开始就包含在Spring框架中。它的 正式名称“SpringWebMVC”来⾃其源模块的名称(Spring-webmvc),但它通常被称为"Spring MVC". 什么是Servlet呢? Ser…...
吃牛羊肉的季节来了,快来看看怎么陈列与销售!
一、肉品陈列基本原则 (一)新鲜卫生 1、保证商品在正确的温度、正确的方式下陈列 (1)正确的温度:冷藏柜-2℃-2℃;冷冻柜库-20℃-18℃ (2)正确的方式: 商品不遮挡冷气出风口&…...
租房业务全流程管理:Spring Boot系统应用
摘要 随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了租房管理系统的开发全过程。通过分析租房管理系统管理的不足,创建了一个计算机管理租房管理系统的方案。文章介绍了租房管理系统的系统分析部分&…...
GCC之编译(7)Linker链接脚本
GCC之(7)Linker链接脚本 Author: Once Day Date: 2024年10月25日 一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦… 漫漫长路,有人对你微笑过嘛… 本文档翻译自GNU LD链接脚本官方手册 参考文章: GNU LD …...
【设计模式系列】适配器模式(九)
目录 一、什么是适配器模式 二、适配器模式的角色 三、适配器模式的典型应用 四、适配器模式在InputStreamReader中的应用 一、什么是适配器模式 适配器模式(Adapter Pattern)是一种结构型设计模式,它允许将不兼容的接口转换为一个客户端…...
C# 文档打印详解与示例
文章目录 一、概述二、PrintDocument 类的使用三、PrintDialog 类的使用四、PageSetupDialog 类的使用五、PrintPreviewDialog 类的使用六、完整示例七、总结 在软件开发过程中,文档打印是一个常见的功能需求。本文将详细介绍如何在C#中实现文档打印,并给…...
Spring Cloud --- Sentinel 熔断规则
熔断规则 慢调用比例 发送10个请求,每个请求理想响应时长为200毫秒。统计1秒钟,如果10个请求响应时间超过200毫秒的比例大于等于10%,则触发熔断,熔断5秒。 异常比例 1秒内,发送请求出现异常率为20%,则触…...
使用爬虫爬取Python中文开发者社区基础教程的数据
👨💻个人主页:开发者-曼亿点 👨💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨💻 本文由 曼亿点 原创 👨💻 收录于专栏:…...
你了解kafka消息队列么?
消息队列概述 一. 消息队列组件二. 消息队列通信模式2.1 点对点模式2.2 发布/订阅模式 三. 消息队列的优缺点3.1 消息队列的优点3.2 消息队列的缺点 四. 总结 前言 这是我在这个网站整理的笔记,有错误的地方请指出,关注我,接下来还会持续更新。 作者&…...
力扣102 二叉树的层序遍历 广度优先搜索
二叉树的层序遍历 题目描述 给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。 示例 1: 输入:root [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15…...
wordpress 赞 插件/优化方案英语
第二章 数据库的基本知识一、名词1. 关系模型* P26(本P35)将数据元素(文件)内部各数据项间的联系和各数据元素间的联系都表示成满足一定条件的二维表形式的模型就是关系模型。2. 数据库 P26以一定的组织方式存储在计算机外存储器中的,相互关联的为多个用户或应用共享…...
网站建设中网站功能描述书功能/全网万能搜索引擎
店铺编辑 首先获取店铺的信息,然后在此基础上进行改动 编写店铺查询queryByShopId(long shopId) 因为在查询店铺时,shop表中只含有 这些PersonInfo,Area,ShopCategory的id,但是需要显示的是所有者的名字,区…...
公司建设官方网站/武汉seo招聘信息
单例模式 最简单但是也挺困难的。 要保证在一个JVM中只能存在一个实例,要考虑到如下的情况: Java能够使用那些方式构建对象Java在创建对象时多线程并发情况下是否仍然只能创建一个实例Java创建对象的方法: new 最常用的,直接使用…...
企业seo顾问服务阿亮/武汉seo网络优化公司
验证尼科彻斯定理,即:任何一个整数m的立方都可以写成m个连续奇数之和。 例如: 1^31 2^335 3^37911 4^313151719 这题也可以用数学公式推理,首项m*(m-1)1,循环m次。 package test;import java.util.Scanner;//尼克彻…...
上海网站建设免费推荐/太原网站制作优化seo
“对于非项目型日志大家一直有疑惑:怎么写?记录到哪一个编号上?”公司对于日志一向有严格要求,凡工作日员工必须登记日志,如请假、调休、公司级的活动等这类非项目型的日志要怎么写?写什么编号?…...
搭建网站框架/上海搜索引擎关键词优化
01百世进凉山助学 这番善举暖了180余名师生的心“因为我刚好遇见你,留下足迹才美丽;如果再相遇,我想我会记得你……”9月8日傍晚,在乃祖库小学上百名学生清亮的歌声和不舍的目送中,凉山州邮政管理局和百世集团一行人踏…...