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:以后能写循环的话就不要写递归
结语
感谢大家阅读我的博客,不足之处欢迎指正,感谢大家的支持
逆水行舟,楫摧而志愈坚;破茧成蝶,翼湿而心更炽!!加油!!