当前位置: 首页 > news >正文

C++进阶--二叉搜索树

文章目录

  • C++进阶--二叉搜索树
    • 概念
    • 算法复杂度
    • 模拟实现
      • 结构定义
      • 插入
      • 查找
      • 删除
      • 剩余的次要接口
          • 中序遍历:
        • 构造,析构,拷贝构造,赋值重载
    • 结语

很高兴和大家见面,给生活加点impetus!!开启今天的编程之路!!
在这里插入图片描述
今天我们开始学习二叉搜索树,为后面map和set打下坚实基础
作者:٩( ‘ω’ )و260
我的专栏:C++进阶,C++初阶,数据结构初阶,题海探骊,c语言
欢迎点赞,关注!!

C++进阶–二叉搜索树

概念

⼆叉搜索树⼜称⼆叉排序树,它或者是⼀棵空树,或者是具有以下性质的⼆叉树:
• 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
• 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
• 它的左右子树也分别为二叉搜索树
⼆叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续的map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值。
我们来看一个图示例:
在这里插入图片描述

算法复杂度

最优情况下,⼆叉搜索树为完全⼆叉树(或者接近完全⼆叉树),其高度为:log2 N
最差情况下,⼆叉搜索树退化为单⽀树(或者类似单⽀),其高度为:N
所以综合而言⼆叉搜索树增删查改时间复杂度为:O(N)

我们以前也学习了一个算法,为二分查找,二叉查找时间复杂度为log2N,二分查找用的缺陷:

1.需要存储在⽀持下标随机访问的结构中,并且有序。
2.插⼊和删除数据效率很低,因为存储在下标随机访问的结构中,插⼊和删除数据⼀般需要挪动数据。

所以,为了处理搜索二叉树的两支结点个数明显不相同的时候,我们提出了平衡二叉树以及红黑树。才能适用于我们在内存中存储和搜索数据

模拟实现

在这里,我们实现的是去重搜索二叉树

结构定义

二叉树的底层是一个一个的结点,结点中又两个指针以及存储的数据(指针域和数据域)。
在搜索二叉树中,数据域可能有各式各样的类型,我们这里统一把他们叫为键值(key)。
来看代码:
搜索二叉树结点类型

template<class K>
struct BSTreeNode{BSTreeNode<Key>*_left = nullptr;//这里会走初始化列表的BSTreeNode<Key>*_right = nullptr;K _key;BSTreeNode(const K& key)//顺便写了构造:_key(key){}
}

虽然我这里结点类型使用的是公有,使用搜索二叉树没有必要去操控结点类型,所以可以设置为公有,而且,我的二叉树封装搜索二叉树结点,迭代器来操作搜索二叉树,实际上实现了变相封装。
搜索二叉树类型

template<class K>
class BSTree{
public:typedef BSTreeNode<k> Node;
private:Node* _root;
}

插入

插入一个数据,首先需要考虑插入的位置的哪里,我们需要通过大小比较来找到需要插入数据的位置,这个位置一定是在叶子结点的,而且还需要考虑要将两个结点进行连接。注意插入数据的时候BSTree为空和非空还要分一下情况
我们来看下面的这个代码用于完成插入操作:

bool Insert(const K&key)
{if(_root==nullptr){_root=new Node(key);reutrn true;}Node*parent=nullptr;Node*cur=_root;while(cur){if(cur->_key<key){parent=nullptr;cur=cur->_right;}else if(cur->_key>key) {parent=cur;cur=cur->left;}else{return false;//去重操作}}//cur为nullptr,parent为叶子结点了Node*newnode=new Node(key);if(cur->_key>key){parent->_left=newnode;}else{parent->_right=newnode;}return true;
}

为什么我这里是进行布尔返回值?因为我要去重,如果是相同数据的话,我就不用再来插入这个数据了

如果我们这里是实现可以插入相同数据的话,其实就是当数据相同的时候,即可以向左走,也可以向右走。

查找

我们进行查找,其实就是进行查找元素与BSTree进行大小比较,最后找到返回true,反之返回false
来看代码:

bool 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 true;//找到了}}return false;
}

最多查找高度次,走到到空,还没找到,这个值不存在。如果不支持插入相等的值,找到x即可返回。

删除

在这里主要是删除是实现的难点,首先,我们先来看需要删除的几种情况:
在这里插入图片描述

如果是删除叶子结点,我们是不是就可以直接将这个叶子结点删除,然后修改我的被删除的叶子结点的父结点的指针指向。
如果我们此时删除没有左孩子的结点,我们就应该让被删除结点的父结点指向被删除结点的下一个结点
我们怎么判断是父结点的left指针修改还是right指针修改呢?
我们来查看被删除的结点是父结点的左孩子还是右孩子即可:

再来看一种情况:
在这里插入图片描述
此时我们发现,被删结点的左子树和右子树都有孩子,此时如何处理?
我们删除这个3之后,该使用哪一个值来替换3的位置呢?同时还得满足二叉搜索树的性质呢?
在这里插入图片描述
因为左子树的最大元素一定是比左子树所有元素都大,由于二叉搜索树的性质。该数又是小于整个右子树的,同理,右子树的最小元素一定是比所有右子树元素小,由于二叉搜索树的性质,该数又是大于整个左子树的。为了演示,这里我们采用右子树的最小元素来书写代码:

我们先来总结一下二叉搜索树的删除问题:

1.要删除结点N左右孩⼦均为空
2.要删除的结点N左孩⼦位空,右孩⼦结点不为空
3.要删除的结点N右孩⼦位空,左孩⼦结点不为空
4.要删除的结点N左右孩⼦结点均不为空

上面四类问题又可以将1融合到2,3类问题中去,因为左右孩子为空是包含在左孩子为空或者右孩子为空的情形中的。
有了分类,我们该如何来实现每一种分类呢?
来看下列图像:
在这里插入图片描述
在这里插入图片描述
此时还需要注意如果我们删除的结点就是根结点,由于根结点没有父结点,所以这种情况需要我们来单独处理删除
来看代码:

bool erase(const Key& key)
{Node*parent=nullptr;Node*cur=_root;while(cur){if(cur->_key<key){parent=nullptr;cur=cur->_right;}else if(cur->_key>key) {parent=cur;cur=cur->left;}else{//找到匹配结点,准备删除结点if(cur->_left==nullptr)//此时左孩子不存在{if(cur==parent)//此时只有一个头结点{root=cur->_right;}else{if(key>parent->_key)//影响父结点的右子树{parent->_right=cur->_right;//上面前提是cur左孩子不存在}else{//影响父结点的左子树parent->_left=cur->_right;}}delete cur;}else if(cur->_right==nullptr)//此时右孩子不存在{if(cur==parent)//此时只有一个头结点{root=cur->_left;}else{if(key>parent->_left)//影响父结点的右子树{parent->_right=cur->_left;}else{//影响父结点的左子树parent->_left=cur->_left;}}delete cur;}else{Node*minright=cur->_right;//先进入右子树顺便找最小Node*pminright=cur;//找到最小结点前一个结点,因为这个也是会被影响的父结点,类似第一种情况while(minright->_left){pminright=minright;minright=minright->_left;//一直向左找最小}std::swap(minright->_key,cur->_key);//交换其中的值//查看影响的是父结点的左子树还是右子树if(pminright->_left==minright)//影响父结点的左子树{pminright->_left=minright->_right;}else{//影响父结点的右子树pminright->_right=minright->_right;}delete minright;}return true;//成功删除}}return false;//没有找到删除数据}
}

上面代码有一个点必须要提一下,为什么下方代码是这个,而不是Node*pminright=nullptr呢?

Node*pminright=cur;

首先,如果是nullptr的话,我必须要进去才能保证pminright被更新,如果没有进入循环,即右子树的根结点就是最小值,此时该右子树无左子树。此时无法进入循环,就会导致空指针的解引用报错:
如下面这种情况:
在这里插入图片描述
注意一定要去理解代码。
而且:为什么我们这里都是pminright->_left=minright->_right;和pminright->_right=minright->_right;都是minright->_right呢?
因为我是一直向minright的_left查找的,出循环的时候minright->_left一定是空,所以,当需要改变父结点指向的时候,只有minright->_right有数据,才能够来进行。

剩余的次要接口

在二叉搜索树中,掌握以上三个接口是至关重要的,下面我们来讲解次要接口:

中序遍历:

特点:二叉搜索树的中序遍历一定是升序的,因为其性质决定的。
来看代码:

public:
void Inorder()
{_Inorder();cout<<endl;
}
private:
void _Inorder(Node* root)
{if (root == nullptr){return;}_Inorder(root->_left);cout << root->_key << " ";_Inorder(root->_right);
}

为什么这里我需要写成递归的形式,一是为了契合stl接口,其次递归需要传参,库中的Inorder并没有传承,所以我们需要再次写一个传参的递归。而且,底层传参设置为私有,因为是给公有函数使用的。

构造,析构,拷贝构造,赋值重载

首先我们需要明白:
拷贝构造有了之后就不会再生成了,造成的影响是我的默认构造函数也不会生成,因为拷贝构造也是构造的一种,所以,如果我们写了拷贝构造,默认构造就必须要写,否则不会生成
来看结果:

BSTree(cosnt BSTree<k>& t)
{_root=Copy(t._root);
}
Node*Copy(Node*root)
{if(root==nullptr){return nullptr;}Node*copy=new Node(root-<_key);copy=Copy(root->left);copy=Copy(root->right);return copy;
}

这里的拷贝构造需要使用递归,因为我需要将整个二叉搜索树给遍历一遍,有前面二叉树的知识,这里问题不大。
这里写成递归的原因还是为了契合stl接口,而且拷贝构造参数只能够来传递这个,想要使用递归,只能够在写出来一个递归函数。

因为有拷贝构造,所以我们这里必须要显示写默认构造,否则编译器不会生成默认构造函数,因为拷贝构造函数也是构造函数,显示写了构造函数就不会再生成构造函数。这里有两种方法:
第一种强制生成:
使用default关键字:
第二种直接显示写即可:

BSTree()=default;//法1
BSTree(){}//法2

来看赋值重载,我们这里还是使用现代式的写法:来看代码:

BSTree<K>& operator=(BSTree<K> t)//这里一定要使用传值传参
{swap(_root,t._root);return *this;
}

随后我们来写析构,因为析构格式是已经被定义好的,但是我们又要写递归将结点全部来删除,所以我们只能够再来写一个函数,来看代码:

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

注意我们这里必须使用后序遍历,不然使用其他两种遍历,子树的指针就找不到了!!
最后还有细节:
1:类模版在外面一定要带模版参数,在类中可以不用带模版参数,但是为了代码可视化,都统一带上模版参数。
2:当需要把iostream转换为一个布尔值时,会调用operator bool函数,为什么是重在bool呢?
因为()(强转符号)和函数调用用的扩容重复了,而且operator ()被仿函数给占用了,所以这里就比较特殊,那什么时候返回假呢?比如我int型但是输入的是浮点型的时候,会判定为假,如果想要结束,就输入ctrl+z+换行即可
如下

int x=0;
while(cin>>x)
{}

3:以后能写循环的话就不要写递归

结语

感谢大家阅读我的博客,不足之处欢迎指正,感谢大家的支持
逆水行舟,楫摧而志愈坚;破茧成蝶,翼湿而心更炽!!加油!!
在这里插入图片描述

相关文章:

  • 万字长文 | Apache SeaTunnel 分离集群模式部署 K8s 集群实践
  • 【Spring】依赖注入的方式:构造方法、setter注入、字段注入
  • WebRTC服务器Coturn服务器部署
  • DB-GPT支持mcp协议配置说明
  • 11、Refs:直接操控元素——React 19 DOM操作秘籍
  • Python跨平台桌面应用程序开发
  • Go语言和Python 3的协程对比
  • docker 里面没有 wget 也 install 不了
  • vscode:Live Server Preview插件
  • load_dataset函数
  • 【C++ 类和数据抽象】构造函数
  • react组件之间如何使用接收到的className(封装一个按钮案例)
  • MongoDB 集合名称映射问题
  • MongoDB索引
  • 【算法】BFS-解决FloodFill问题
  • React项目添加react-quill富文本编辑器,遇到的问题,比如hr标签丢失
  • Apache SeaTunnel:新一代开源、高性能数据集成工具
  • QTextDocument 入门
  • 屏幕适配常见BUG与兼容性问题
  • 7N60-ASEMI无人机专用功率器件7N60
  • 大家聊中国式现代化|刘亮:因地制宜发展新质生产力,推动经济高质量发展
  • 常方舟评《心的表达》|弗洛伊德式精神分析在我们时代的延展
  • 民政部:从未设立或批准设立“一脉养老”“惠民工程”项目,有关App涉嫌诈骗
  • 湖南平江发生人员溺亡事件,已造成4人死亡
  • 商务部:支持“来数加工”等新业态新模式,发展游戏出海业务
  • 世界史圆桌|16-18世纪的跨太平洋贸易