【C++】二叉搜索树

二叉搜索树

  • 前言
  • 正式开始
    • 模拟实现
      • 树节点以及树框架
      • 中序遍历
      • 查找
      • 删除
    • 递归实现增删查
    • 析构
    • 拷贝构造
    • 赋值重载
    • 时间复杂度分析
    • 应用场景
    • 两道题

在这里插入图片描述

前言

本来想先把搁置了一个月的Linux讲讲的,但是里面有些内容需要用到一些比较高级的数据结构,用C写的话比较麻烦,所以还是接着我前面的C++讲。

本篇主要讲二叉搜索树,先说概念,然后直接上手实现。再给一些生活中的场景,最后用这里的二叉搜索树来解前面我写数据结构阶段的两道链表题。

正式开始

二叉搜索树(搜索二叉树),也叫二叉排序树。如果某棵二叉搜索树不是空树,则其具有以下性质:

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树

简单来说就是 左 < 根 < 右。搜索树不允许有重复值,所以没有相等的情况。

二叉搜索树是第一个二叉树的应用,还是比较有用的。概念讲完了,就直接开始实现。

模拟实现

就实现三个功能,一般的数据结构都是增删查改四个基本功能,这里二叉搜索树少了一个改的功能,具体为什么各位等会看其余的三个实现就懂了。

二叉搜索树分为两类,一类是key模型,一类是key/value模型,至于什么意思暂时讲不了,但是你们先看模拟实现就行了, 这里先实现key模型的,看完模拟实现就懂了。

树节点以及树框架

二叉搜索树的英文名字叫binary search tree,缩写就用的是BST。

先是树节点,这模版中的模版参数用的是K,而不是平常的T,主要是为了标志出这里的实现是key模型的实现:

在这里插入图片描述

上面的是struct而不是class是因为等会实现的时候节点中的左右孩子指针和val一直都要用到。跟前面我在list的模拟实现那篇中同理。

然后就是树的框架:
在这里插入图片描述

在里面typedef一下树节点,用起来比较方便。初始情况下root为空。

然后就可以写增删查了。

就是往树里面插入。不过这里有点要求。就是插入树节点的时候要保证 左 < 根 < 右。所以要先找到合适的位置,然后再在该位置上插入。

我们就用 int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13}; 这几个数来挨个插入。

先把图解画出来:
在这里插入图片描述

那么上面的这棵树就是二叉搜索树,如上面的过程能看懂,那么我觉得二叉搜索树的插入思想你就明白了。

就是找合适的位置,插入即可。

先给个接口:
在这里插入图片描述
用bool作为返回值,因为前面说了搜索树中不能有相同的数值。如果有了相同的数值就返回false。

中间要创建节点值为val的新节点,所以我们可以在BSTreeNode中写一个构造:
在这里插入图片描述

然后就是找合适位置了:
在这里插入图片描述
cur经过如上代码,就可找到要插入的位置。

如果数据结构学的不是很扎实的同学可能会犯如下错误:
在这里插入图片描述
这种情况下直接返回。

屏幕前的你知道哪里出问题了吗?
仔细捋一捋就能发现,cur本来已经找到了该插入的位置的,但是new了之后cur的值就变成了val新节点的地址了,这里根本就没有插入,就只是将cur不断地赋值而已。

那么要改一改,插入的时候要插入到合适的位置,要插入到某一个节点的孩子位置,最重要的是要知道插入位置的父节点。

所以找插入位置的过程要不断记住路程中的父节点,这样才能保证插入的位置是在树上的,而不是随机找个节点插。

最终代码如下:
在这里插入图片描述

再来写一个中序遍历验证一下:

中序遍历

如果写成下面这样:
在这里插入图片描述
调用的时候就有点小问题。

这样的写法在用对象调用的时候必须要将树的根节点指针传过去,但是又有一个问题,我实现的树里面根节点是私有的。

想要解决的话可以给一个接口来专门返回根节点的地址;或者还可以用友元。

有的同学说可以给缺省参数,将函数的缺省参数给为_root,这样的做法是错误的:
在这里插入图片描述

这里有一个最优解法,就是搞一个子函数。
像下面这样:

在这里插入图片描述

就可以直接不传参调用InOrder。
在这里插入图片描述
因为不支持插入重复元素,所以这里绝对不会打印出重复元素。中序打印出来的结果完全就是排好序的。因为左根右的遍历方式打印出来就是有序的,不理解的自己想一想。

然后来说查找。

查找

查找是这三个里面最简单的。

在这里插入图片描述

这里不需要返回节点什么的,只要能判断在不在就可以了,这也是key模型的关键所在,等会也会讲对应的应用场景。

测试一下:
在这里插入图片描述

再来说删除。

删除

这个最麻烦,主要是删除一个数后要保持其仍然是一个二叉搜索树。

被删除的节点可以分三种情况:

  1. 没有孩子
  2. 有一个孩子
  3. 有两个孩子

分别来画图看看:

没有孩子

节点删除之后将树中的该位置改为nullptr就行。
在这里插入图片描述
实现起来的话,先找到13,删除13,再让14的左指向空。

有一个孩子

子替换父即可。
在这里插入图片描述

实现起来的话,就是先找到14,然后让10的右指向13,再删除14。

有两个孩子

删除的时候要用到替换法。
最麻烦的就在这里。

两种解决方式:

  1. 让删除节点的左树中最大的替换到删除节点处
  2. 让删除节点的右树中最小的替换到删除节点处

观看理论比较晦涩,看图:
在这里插入图片描述
这样替换下来,仍能够保持其是一棵二叉搜索树。

实现起来的话,两种方法:

  1. 左子树:先找到3,再去3的左子树中找最大值1,然后让二者的值交换,这
    样1就跑到了根,3就跑到左子树上了,再删除交换后的3处的节点。
  2. 右子树:先找到3,再去3的左子树中找最小值4,然后让二者的值交换,这样4就跑到了根,3就跑到右子树上了,再删除交换后的3处的节点。

再来个例子:
在这里插入图片描述

树的根节点的删除比较特殊,这里没看懂的话没关系,等会会详谈。

根据上面的思想,删除两个孩子的节点方法可以总结如下:

  1. 先找到删除的节点
  2. 删除的时候只用选择 去左树中找最大值 或者 去右树中找最小值 就行了。
    如果去左树中,那么就是左树的最右边,就是左树的最大值。
    如果去右树中,那么就是右树的最左边,就是右树的最小值。

上面孩子的三种情况都要先找到删除的节点,然后再分情况讨论即可。
那就可以写代码了:

因为删除后要让删除的位置为空,所以要定义出一个不断更新的父节点,来找到最后删除位置的父节点。

在这里插入图片描述

根据二叉搜索树的特性,先找到节点:

在这里插入图片描述

然后再分孩子的情况讨论,我们这里可以把没有孩子的和有一种孩子的放到一块,先不说为什么,各位看图:

没有孩子,比如删13的话,此时就是这样:
在这里插入图片描述
删除13,然后让14的左为空,可以直接让14指向13的任意一个节点,因为13的任意节点的值都为空。

有一个孩子,比如删14的话,此时就是这样:
在这里插入图片描述
如果删除14的话,可以让10的右指向14的左13,然后再删除14。

二者都能让 parent节点的左/右 指向 cur的左/右 ,就能实现替换这一过程,替换之后再删除cur即可。

如下:
在这里插入图片描述

然后内部还要分cur是parent的左还是右:
在这里插入图片描述
上面删除cur的地方代码冗余了,等会再搞。

但是还有问题,如果是删除根节点的话,上面的代码就出bug了。
比如说这样:
在这里插入图片描述

因为如果val就是根节点的值话,cur的while循环就进不去,那么parent此时就是nullptr,上面的代码就解引用空指针。所以还要分parent是否为空的情况:

在这里插入图片描述

再来说左右都不为空的节点,对应删除3:在这里插入图片描述
这里我们以找右树的最小值为例:

右子树的最左边就是最小值:
在这里插入图片描述

然后将3和4的值交换,然后再删除min节点就可以了,但是还要将6的左置空,所以又得产生一个不断变换的父节点来记录min的父节点。

所以最终就是这样:
在这里插入图片描述

这里不用判断parent是否为空的情况,因为节点的数值交换了。

代码:
在这里插入图片描述

这样删除工作就做好了,可以说还是比较麻烦的。

测试一下:
在这里插入图片描述
成功。

这里把完整的删除代码给出来:

bool Erase(const K& val)
{Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_val < val){parent = cur;cur = cur->_right;}else if (cur->_val > val){parent = cur;cur = cur->_left;}else // cur 就是要删除的节点{if (cur->_left == nullptr){ // 左树为空的情况,分两种// 1.包含了左右都为空 2.只有左为空 对应到图中就是删除13和删除10// 判断val是否为_root的valif (parent == nullptr) // 也可用 cur == _root 来判断{ // 这里cur左右都为空的情况也成立_root = cur->_right;}else{// 看cur是parent的左树还是右树if (cur == parent->_left) // cur是parent的左树parent->_left = cur->_right;else // cur是parent的右树parent->_right = cur->_right;}delete cur;cur = nullptr;}else if (cur->_right == nullptr){ // 右树为空的情况,上面已经包含左右都为空的情况,所以这里只有一种情况// 就是只有右树为空的情况,对应到图中就是删除14// 判断val是否为_root的valif (parent == nullptr){_root = cur->_left;}else{// 看cur是parent的左树还是右树if (cur == parent->_left) // cur是parent的左树parent->_left = cur->_left;else // cur是parent的右树parent->_right = cur->_left;}delete cur;cur = nullptr;}else { // 左右都不为空Node* min = cur->_right;Node* parentMin = cur;// 去右树中找最小值while (min->_left){parentMin = min;min = min->_left;}swap(min->_val, cur->_val);if (parentMin->_left == min)parentMin->_left = min->_right;elseparentMin->_right = min->_right;delete min;min = nullptr;}// 删除成功return true;}}// 没有删除的节点return false;
}

三个功能均已经实现了,我们还可以用递归的方式实现。

递归实现增删查

先说最简单的查。

在这里插入图片描述

再说插入

直接看代码:
在这里插入图片描述

这里非常巧妙运用了引用。

第一个参数root类型为Node*&,什么意思呢,就是一个Node的引用,也就是一个Node变量的别名。

当我们找到了要插入的位置的时候,一定是一个子节点,传过来的一定是root->_left 或者 root->_right 。所以引用的就是父节点左/右的指针。

所以当root为空的时候就是要插入的时候,这时候root就是父节点左/右的指针,就可以直接用new将开辟的空间赋值给root,等价于直接将开辟的空间赋值给了父节点左/右的指针。

先给出大致框架:
在这里插入图片描述

然后跟上面非递归的删除一样,也要判断孩子的情况:
在这里插入图片描述

又因为我们删除节点之后还要置空,但是递归想要找父节点还要多传一个参数,我们此时就可以再将参数改为&的。也就是Node*& root。这样root就直接变成了父节点的左/右指针了。

这里也不需要再考虑删除的位置是否为数的根了,看代码:
在这里插入图片描述

整个递归erase的代码如下:

bool _EraseR(Node*& root, const K& val)
{if (root == nullptr)return false;if (root->_val == val){if (root->_left == nullptr){Node* right = root->_right;delete root;root = right;}else if (root->_right == nullptr){Node* left = root->_left;delete root;root = left;}else{Node* min = root->_right;while (min->_left)min = min->_left;swap(min->_val, root->_val);_EraseR(root->_right, val);}return true;}if (root->_val > val)return _EraseR(root->_left, val);if (root->_val < val)return _EraseR(root->_right, val);
}

到这里这三个功能正式讲完。

注意上面的所写的函数都是子函数,都是私有的,公有的只提供了接口。
在这里插入图片描述

再说点别的。

析构

给出如下代码:

在这里插入图片描述
运行结束之后会崩掉吗?

答案是不会,因为我还没有写析构。

那么二叉树的析构,很简单。后序递归即可。

但是析构函数没有参数,所以也是搞一个子函数就行。
在这里插入图片描述
然后上面的代码运行起来就崩掉了,因为拷贝构造是默认生成的,内置类型做浅拷贝。只是把cp的根节点指向了bst的根节点上,两个值相同。所以析构就崩掉了。

拷贝构造

也是递归构造,要写子函数。
在这里插入图片描述

在这里插入图片描述

测试:
在这里插入图片描述
出错了,编译器说我没有默认的构造函数可用。
因为生成了一个构造函数之后编译器就不再提供默认的构造函数了。拷贝构造也算构造。所以此时加上一个构造函数就行。

此时运行就崩不了。

赋值重载

这个还是老方法,直接参数传值,交换即可。

在这里插入图片描述

下面说说引用场景。

时间复杂度分析

二叉搜索树,听名字就能知道主要是用来搜索的。那么其查找的时间复杂度是多少呢?

可能有的同学认为是logN,其实不是,当树不是接近满二叉树或者完全二叉树时,效率可能比较低,比如棵单边树:
在这里插入图片描述
这样查找效率就很低了,就是O(N)的。

总的来说二叉搜索树的查找效率是取决于树形状的。

所以二叉搜索树控制插入的根节点的值非常重要,但是一般很难决定。后面还有AVL树来平衡整棵树。

应用场景

上面写的是key模型的,主要用来判断关键字在不在,比如说

  1. 学生刷卡进宿舍楼。
    这里就是学生卡中记录学生的某一项信息,比如学号,记录到卡的芯片中,然后数卡的时候通过二叉搜索树来查找是否存在,如果二叉搜索树比较均匀的话(满或完全二叉树),查找的效率就非常高,当然,AVL树,比二叉搜索树方便点,但原理都一样。
  2. 检查一段英文中每个单词拼写是否正确。
    记录正确的拼写,然后查找单词是否存在就行了。

还有一种模型是key/value模型,其原理是通过key来找value。key模型和key/value模型非常相似,key/value模型还是通过key比较,value只是一个附加项。例子有:

  1. 英文单词译为中文

  2. 统计……出现的次数

这里简单写一个key/value模型

代码如下:

template<class K, class V>
struct BSTreeNode
{BSTreeNode<K, V>* _left;BSTreeNode<K, V>* _right;K _key;V _value;BSTreeNode(const K& key, const V& value):_left(nullptr), _right(nullptr), _key(key), _value(value){}
};template<class K, class V>
class BSTree
{typedef BSTreeNode<K, V> Node;
public:bool Insert(const K& key, const V& value){if (_root == nullptr){_root = new Node(key, value);return true;}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{return false;}}cur = new Node(key, value);if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;}Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else{return cur;}}return nullptr;}bool Erase(const K& key){//...return true;}void InOrder(){_InOrder(_root);cout << endl;}
private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << ":" << root->_value << endl;_InOrder(root->_right);}
private:Node* _root = nullptr;
};

拿第一个例子:

在这里插入图片描述

在这里插入图片描述

插入的时候是按照英文字符串进行比较的。

两道题

这两道题说一下思路:

链表相交

key模型,先入一个链表,再遍历另一个链表查找某节点是否存在,若存在就返回存在的节点,不存在就继续遍历链表,直至遍历完毕。

复制带随机指针的链表

key/value模型,建立原节点和拷贝节点的映射关系。

比如:

在这里插入图片描述

黑色为原节点,蓝色为拷贝节点。1和1,2和2,3和3,建立映射。

1的random为3,那么蓝色的1random也为3,我们可以通过映射关系,通过黑色的3找蓝色的3,继而找到蓝色的random,然后连接1、3即可。其余同理。

到此结束。。。

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

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

相关文章

【业务功能篇61】SpringBoot项目流水线 dependencyManagement 标签整改依赖包版本漏洞问题

业务场景&#xff1a;当前我们项目引入了公司自研的一些公共框架组件&#xff0c;比如SSO单点登录jar包&#xff0c;文件上传服务jar包等公共组件&#xff0c;开发新功能&#xff0c;本地验证好之后&#xff0c;部署流水线&#xff0c;报出一些jar包版本的整改漏洞问题&#xf…

二叉树OJ(C)

文章目录 1.单值二叉树1.1法一&#xff1a;无返回值1.2法二&#xff1a;有返回值 2.相同的树3.对称二叉树4.二叉树的前序遍历5.二叉树的中序遍历6.二叉树的后序遍历7.另一棵树的子树8.二叉树遍历 1.单值二叉树 1.1法一&#xff1a;无返回值 struct TreeNode {int val;struct …

Virtualbox虚拟机中Ubuntu忘记密码

1、首先重新启动Ubuntu系统&#xff0c;鼠标快速点一下Virtualbox虚拟机窗口获取焦点&#xff0c;然后按住shift键&#xff0c;以调出grub启动菜单。 2、根据提示按下键盘E键进入编辑模式&#xff0c;向下移动光标&#xff0c;将如下"ro quiet splash $vt_handoff"部…

SpringBoot集成jasypt,加密yml配置文件

SpringBoot集成jasypt&#xff0c;加密yml配置文件 一、pom配置二、生成密文代码三、配置3.1、yml加密配置3.2、密文配置3.3、启动配置3.4、部署配置 四、遇到的一些坑 最新项目安全检测&#xff0c;发现配置文件中数据库密码&#xff0c;redis密码仍处理明文状态 一、pom配置…

一套AI+医疗模式的医院智慧导诊系统源码:springboot+redis+mybatis plus+mysql

一套AI医疗模式的医院智慧导诊系统源码 相关技术&#xff1a; 技术架构&#xff1a;springbootredismybatis plusmysqlRocketMQ 开发语言&#xff1a;java 开发工具&#xff1a;IDEA 前端框架&#xff1a;Uniapp 后端框架&#xff1a;springboot 数 据 库&#xff1a;mys…

【win11+vs 2017+OpenCV4.5.5+Qt5.12配置】解决了过程中遇到的小问题

0.版本选择 由于Qt5无法与最新的vs2022兼容&#xff0c;扩展工具中一直显示不可用&#xff0c;所以将vs降级成vs2017。 在安装Qt的过程中&#xff0c;会选择安装Qt套件&#xff0c;其中就的MCVS 2017&#xff0c;说明vs2017是与qt兼容的。 当然也可以用qt creator这一原生IDE。…

LEARNING TO EXPLORE USING ACTIVE NEURAL SLAM 论文阅读

论文信息 题目&#xff1a;LEARNING TO EXPLORE USING ACTIVE NEURAL SLAM 作者&#xff1a;Devendra Singh Chaplot, Dhiraj Gandhi 项目地址&#xff1a;https://devendrachaplot.github.io/projects/Neural-SLAM 代码地址&#xff1a;https://github.com/devendrachaplot/N…

ES6基础知识九:你是怎么理解ES6中Module的?使用场景?

一、介绍 模块&#xff0c;&#xff08;Module&#xff09;&#xff0c;是能够单独命名并独立地完成一定功能的程序语句的集合&#xff08;即程序代码和数据结构的集合体&#xff09;。 两个基本的特征&#xff1a;外部特征和内部特征 外部特征是指模块跟外部环境联系的接口…

Stable Diffusion AI绘画学习指南【插件安装设置】

插件安装的方式 可用列表方式安装&#xff0c;点开Extensions 选项卡&#xff0c;找到如下图&#xff0c;找到Available选项卡&#xff0c;点load from加载可用插件&#xff0c;在可用插件列表中找到要装的插件按install 按扭按装&#xff0c;安装完后(Apply and restart UI)应…

15、两个Runner初始化器和 springboot创建非web应用

两个Runner初始化器 两个Runner初始化器——主要作用是对component组件来执行初始化 这里的Component组件我理解为是被Component注解修饰的类 Component //用这个注解修饰的类&#xff0c;意味着这个类是spring容器中的一个组件&#xff0c;springboot应用会自动加载该组件。 …

【原创】IPTVC2实现方案(文末有demo)

前言: 名词解释: IPTVC2, 全称: 央视国际节目定价发布接口规范,标准版本当前最新为2.7.12 附赠资源链接&#xff0c;侵删:规范 规范中提供的样例&#xff0c;实现基于axis1.4(2006的时代宠物) 基于axis1版本的实现参考: Spring boot 集成Axis1.4 &#xff0c;使用wsdd文件发…

【CSDN】

欢迎使用Mark编辑器 你好&#xff01; 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章&#xff0c;了解一下Markdown的基本语法知识。 新的改变 我们对Markdown编辑器进行了一些功能拓展与语法支持&#xff0c…

自动驾驶感知系统-全球卫星定位系统

卫星定位系统 车辆定位是让无人驾驶汽车获取自身确切位置的技术&#xff0c;在自动驾驶技术中定位担负着相当重要的职责。车辆自身定位信息获取的方式多样&#xff0c;涉及多种传感器类型与相关技术。自动驾驶汽车能够持续安全可靠运行的一个关键前提是车辆的定位系统必须实时…

【数学建模】——拟合算法

【数学建模】——拟合算法 拟合算法定义&#xff1a;与插值问题不同&#xff0c;在拟合问题中不需要曲线一定经过给定的点。拟合问题的目标是寻求一个函数&#xff08;曲线&#xff09;&#xff0c;使得该曲线在某种准则下与所有的数据点最为接近&#xff0c;即曲线拟合的最好&…

好用的Linux远程工具

你好&#xff0c;我是Martin&#xff0c;今天给大家介绍几款主流的远程工具。 远程工具介绍 关于远程连接的用户分类时这样的&#xff0c;通常需要进行远程连接的人有两类&#xff0c;一类是系统管理员&#xff0c;另一类是普通的用户。远程连接工具是一些可以让你通过网络连接…

2023年华数杯建模思路 - 复盘:光照强度计算的优化模型

文章目录 0 赛题思路1 问题要求2 假设约定3 符号约定4 建立模型5 模型求解6 实现代码 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 问题要求 现在已知一个教室长为15米&#xff0c;宽为12米&#xff0…

Nacos配置中心设置Mongodb

目录 1.common模块导入nacos config依赖 2.common模块新建bootstrap.yaml 3.在自己的模块导入common模块依赖 4.打开nacos新建配置&#xff0c;发布 5.运行服务并测试 效果&#xff1a;在部署完成后&#xff0c;其他人可以自动连接到你本地mongoDB数据库&#xff0c;无需再…

建模教程:如何利用3ds Max 和 After Effects 实现多通道渲染和后期合成

推荐&#xff1a; NSDT场景编辑器 助你快速搭建可二次开发的3D应用场景 1. 创建基本场景 步骤 1 打开 3ds Max。 打开 3ds Max。 步骤 2 我做了一个简单的场景。我放了三个 彼此之间有一定距离的物体。 制作对象 步骤 3 按 Ctrl-C 键 在透视视图中创建摄影机。 创建相机 …

Android性能优化—LeakCanary内存泄漏检测框架分析。

一、什么叫内存泄漏、内存溢出&#xff1f; 内存溢出(out of memory)&#xff1a;是指程序在申请内存时&#xff0c;没有足够的内存空间供其使用&#xff0c;出现out of memory&#xff1b;比如申请了一个10M的Bitmap&#xff0c;但系统分配给APP的连续内存不足10M&#xff0c…

P3372 【模板】线段树 1(内附封面)

【模板】线段树 1 题目描述 如题&#xff0c;已知一个数列&#xff0c;你需要进行下面两种操作&#xff1a; 将某区间每一个数加上 k k k。求出某区间每一个数的和。 输入格式 第一行包含两个整数 n , m n, m n,m&#xff0c;分别表示该数列数字的个数和操作的总个数。 …