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:以后能写循环的话就不要写递归

结语

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值