二叉搜索树(BST)

二叉搜索树的概念

一棵树,可以为空,如果不为空,则需要满足一下性质:1.非空左子树的所有键位小于其根节点的键值。2.非空右子树的所有键大于其根节点的键值。3.左右子树都是二叉搜索树。

二叉搜索树的结构

结点类:

template<class K>
struct BSTreeNode
{
	K _key;
	BSTreeNode<K>* leftNode;
	BSTreeNode<K>* rightNode;
	BSTreeNode(const K& key) :_key(key), 
		leftNode(nullptr), rightNode(nullptr) {};
}; 

BST树类的成员变量:
在这里插入图片描述

Insert

首先,我们的插入是传入一个节点中val的类型k的数据进入到Inser函数中实现插入逻辑。当树为空的时候,插入的那个元素就是直接作为根节点,当树中已经有元素的时候,每一次的插入都需要给插入的元素寻找一个符合二叉搜索树的定义。比如如下图:
在这里插入图片描述
在这里插入图片描述
插入的过程就是每一次插入一个元素都要去对他的根节点去判断,如果大于根节点就让往左边走,然后再去判断,直到根节点为空为止,让然后把要插入元素放到该位置。注意:因为是找到空才插入一般插入的元素都是在叶子节点
代码实现:

	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		
		Node* cur = _root, *parent = _root;
		while (cur)
		{
			parent = cur;
			if (key > cur->_key)
			{
				cur = cur->rightNode;
			}
			else
			{
				cur = cur->leftNode;
			}
		}
		cur = new Node(key);
		if (cur->_key > parent->_key)
		{
			parent->rightNode = cur;
		}
		else
		{
			parent->leftNode = cur;
		}
		return true;
	}

代码解析:用一个临时变量cur来实现判断和遍历的逻辑(寻找一个适合插入节点的位置),当我们的cur遍历到空的root的时候就是那个合适的位置,因为在上面的每一次比较已经经过了符合BST的定义的判断。但是在链表插入不仅是在该节点的数据存储位置存储我们的数据,还有让他与他的父结点连接起来,所以我们需要parent这样的一个变量来记录cur对应的父结点。

InOrder

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

	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		
		_InOrder(root->leftNode);
		cout << root->_key << " ";
		_InOrder(root->rightNode);
	}
	void InOrder()
	{
		_InOrder(_root);
	}

中序遍历逻辑:根据左根右的方式来实现递归逻辑,写递归必须要明确的点就是有结束条件。在这段代码中有两个结束条件一个是root为空,一个是走到函数的末尾。这两个结束都会当递归返回到他前一个栈帧接着执行它未完成的逻辑。如何理解上面递归输出递归的那三条语句呢?大概的抽象逻辑就是按照左根右顺序不停地把把每个递归栈帧中的root输出(在这里就不会递归展开图了,麻烦小伙伴们自己画)
还有这里值得注意的是为什么写递归要套多一层,因为这里不套多一层的话,在外面调用这个函数无法传入Node* root 因为在数中的根节点是private修饰的成员变量,所以这里写成用在类里面使用root,则解决了在外面访问不了root的问题。

Find

示例代码:

bool Find(const K& key)
	{
		Node* cur = _root;//用于遍历
		
		while (cur)
		{
			if (key > cur->_key)
			{
				cur = cur->rightNode;
			}
			else if (key < cur->_key)
			{
				cur = cur->leftNode;
			}
			else
			{
				return true;
			}
		}
		return false;
	}

查找就是插入功能中一部分代码拿出来改了一下

Erase

思路:
1.搜索出删除结点
2.判断删除情况
3.根据情况进行删除
难点在于,如何删除后这个树依然保持搜索二叉树的定义:
1.对于只有一个孩子的结点来说,删除只需要删除该结点然后把他的孩子结点交给被删除结点的父结点连接起来。如图:
在这里插入图片描述
2.无孩子的结直接删除,这里就不画图了
3.有两个孩子的结点就将他与他的左子树的最大结点或者右子树最小结点交换(替换法)如图:
在这里插入图片描述

示例代码:

	bool Erase(const K& key)
	{
		assert(_root);
		//查找删除结点
		Node* cur = _root;
		Node* parent = cur;
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->rightNode;
			}
			else if(key < cur->_key)
			{
				parent = cur;
				cur = cur->leftNode;
			}
			else//相等,判断删除情况
			{
				if (cur->leftNode == nullptr)//证明删除结点的孩子右边或者没有孩子
				{
					//删除
					if (cur->_key == parent->leftNode->_key)
					{
						parent->leftNode = cur->rightNode;
					}
					else
					{
						parent->rightNode = cur->rightNode;
					}
					delete cur;
					return true;
				}
				else if (cur->rightNode == nullptr)
				{
					if (cur->_key == parent->leftNode->_key)
					{
						parent->leftNode = cur->leftNode;
					}
					else
					{
						parent->rightNode = cur->leftNode;
					}
					delete cur;
					return true;
				}
				else//两边都有孩子
				{
					//找右子树最小节点
					Node* swapNodeParent = cur;
					Node* swapNode = cur->rightNode;
					while (swapNode->leftNode)
					{
						swapNodeParent = swapNode;
						swapNode = swapNode->leftNode;
					}
					std::swap(cur->_key, swapNode->_key);
					//删除swapNode
					if (swapNodeParent->leftNode->_key == swapNode->_key)
					{
						swapNodeParent->leftNode = nullptr;
					}
					else
					{
						swapNodeParent->rightNode = nullptr;
					}
					delete swapNode;
					return true;
				}
			}
		}
		return false;
	}

大致逻辑在上面思路已经说过了,这里的代码实现主要是使用了几个变量来记录父结点或者搜索可以交换的结点,然后用cur->leftNode == nullptr(cur->rightNode == nullptr)来确定要删除的结点的孩子在右边还是左边,并且有一边肯定为空,这样就可以判断出该使用直接删除节点连接孩子的方法,也便于我要把那一边的孩子交给被删除结点的父结点去管理,然后再确定被删除结点是父结点那一结点,以便于确定被删除结点的孩子要连接到被删除结点的父结点的那一边。
cur->leftNode == nullptr(cur->rightNode == nullptr)的筛分下,剩下要处理的场景就是有两个孩子的结点删除,对于这样的结点我们要用替换法去实现,去和谁替换呢?去和右子树的最小结点或者是左子树的最大结点交换(上面的代码使用的是右子树的最小结点)其实右子树最小的结点就是右子树的最左结点,而左子树的最结点就是左子树的最右结点。所以首先也找到用于交换的结点然后进行交换,并且把交换后的结点删除,然后其实这里还有一点,因为最左结点和最右结点一定是叶子节点或者是只有右孩子的结点,所以直接让交换结点的父结点直接连接nullptr。
其实上面代码在某些场景下还是没有处理好,如图:
在这里插入图片描述
错误代码:

bool Erase(const K& key)
	{
		assert(_root);
		//查找删除结点
		Node* cur = _root;
		Node* parent = cur;
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->rightNode;
			}
			else if(key < cur->_key)
			{
				parent = cur;
				cur = cur->leftNode;
			}
			else//相等,判断删除情况
			{
				//删除根节点
				if (parent == cur)
				{
					if (parent->leftNode == nullptr)
					{
						_root = parent->rightNode;
					}
					else
					{
						_root = parent->leftNode;
					}
					delete parent;
					return true;
				}

				if (cur->leftNode == nullptr)//证明删除结点的孩子右边或者没有孩子
				{
					//删除
					if (cur->_key == parent->leftNode->_key)
					{
						parent->leftNode = cur->rightNode;
					}
					else
					{
						parent->rightNode = cur->rightNode;
					}
					delete cur;
					return true;
				}
				else if (cur->rightNode == nullptr)
				{
					if (cur->_key == parent->leftNode->_key)
					{
						parent->leftNode = cur->leftNode;
					}
					else
					{
						parent->rightNode = cur->leftNode;
					}
					delete cur;
					return true;
				}
				else//两边都有孩子
				{
					//找右子树最小节点
					Node* swapNodeParent = cur;
					Node* swapNode = cur->rightNode;
					while (swapNode->leftNode)
					{
						swapNodeParent = swapNode;
						swapNode = swapNode->leftNode;
					}
					std::swap(cur->_key, swapNode->_key);
					//删除swapNode
					if (swapNodeParent->leftNode->_key == swapNode->_key)
					{
						swapNodeParent->leftNode = nullptr;
					}
					else
					{
						swapNodeParent->rightNode = nullptr;
					}
					delete swapNode;
					return true;
				}
			}
		}
		return false;
	}

错误校正:
错误点1:因为最左结点和最右结点一定是叶子节点或者是只有右孩子的结点,所以直接让交换结点的父结点直接连接nullptr。 不能直接置为空,因为还有可能是有右孩子结点所以,应该改成等于swap->rightNode
错误点2:if (swapNodeParent->leftNode->_key == swapNode->_key) 这个比较不能用key去比较因为不排除有key相等但是结点不一样的情况,所以要改成指针比较
正确代码:

	bool Erase(const K& key)
	{
		assert(_root);
		//查找删除结点
		Node* cur = _root;
		Node* parent = cur;
		while (cur)
		{
			if (key > cur->_key)
			{
				parent = cur;
				cur = cur->rightNode;
			}
			else if(key < cur->_key)
			{
				parent = cur;
				cur = cur->leftNode;
			}
			else//相等,判断删除情况
			{
				if (cur->leftNode == nullptr)//证明删除结点的孩子右边或者没有孩子
				{
					//删除根节点
					if (parent == cur)
					{
						if (parent->leftNode == nullptr)
						{
							_root = parent->rightNode;
						}
						else
						{
							_root = parent->leftNode;
						}
						delete parent;
						return true;
					}
					//删除
					if (cur == parent->leftNode)
					{
						parent->leftNode = cur->rightNode;
					}
					else
					{
						parent->rightNode = cur->rightNode;
					}
					delete cur;
					return true;
				}
				else if (cur->rightNode == nullptr)
				{
					//删除根节点
					if (parent == cur)
					{
						if (parent->leftNode == nullptr)
						{
							_root = parent->rightNode;
						}
						else
						{
							_root = parent->leftNode;
						}
						delete parent;
						return true;
					}
					if (cur == parent->leftNode)
					{
						parent->leftNode = cur->leftNode;
					}
					else
					{
						parent->rightNode = cur->leftNode;
					}
					delete cur;
					return true;
				}
				else//两边都有孩子
				{
					//找右子树最小节点
					Node* swapNodeParent = cur;
					Node* swapNode = cur->rightNode;
					while (swapNode->leftNode)
					{
						swapNodeParent = swapNode;
						swapNode = swapNode->leftNode;
					}
					std::swap(cur->_key, swapNode->_key);
					//删除swapNode
					if (swapNodeParent->leftNode == swapNode)
					{
						swapNodeParent->leftNode = swapNode->rightNode;
					}
					else
					{
						swapNodeParent->rightNode = swapNode->rightNode;
					}
					delete swapNode;
					return true;
				}
			}
		}
		return false;
	}

FindR(递归实现)

示例代码:

	bool _FindR(Node* root,const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (key > root->_key)
		{
			return _FindR(root->rightNode, key);
		}
		else if(key < root->_key)
		{
			return _FindR(root->leftNode, key);
		}
		else
		{
			return true;
		}
	}
	
	bool FindR(const K& key)
	{
		return _FindR(_root,key);
	}

代码实现逻辑:
在这里插入图片描述
核心思想就是不停地更换参数root,让root作为参数去判断,本质还是root的key值大于就往右走,小于往左走,root为空就代表走到了尽头,相等就返回true,但是在递归中返回参数,需要层层返回上去,因为每一次的返回,只是把值返回到他的上一层栈帧,所以需要在 _FindR(root->leftNode,key)之前加上return。

InsertR(递归实现)

	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		//选择合适的插入位置
		if (key > root->_key)
		{
			//cout << root->_key << endl;
			return _InsertR(root->rightNode, key);
		}
		else
		{
			//cout << root->_key << endl;
			return _InsertR(root->leftNode, key);
		}
	}
	
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}

代码实现逻辑:
和循环写的一样,如果key大于root->key的话就往右边走,反之左边,不过这里是用递归不断更改传参来实现“走”的逻辑,当root走到nullptr的时候就证明找到适合插入的位置。这里值得注意的是,如何实现插入逻辑?如何将给树添加新的结点。答案是通过传引用作为形参,这时root就是这个树的别名,我们可以通过直接改变这个引用来去改变树这样的形式实现插入。
这里不禁引出一个问题?在用循环写插入时候是否可以写成引用的方式。
在这里插入图片描述
答案是不可以的,因为在C++中引用是不能改变他所引用的对象的。
在这里插入图片描述

EraseR(递归实现)

示例代码:

	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)//找不到删除结点
		{
			return false;
		}
		//查找删除结点
		if (key < root->_key)
		{
			return _EraseR(root->leftNode, key);
		}
		else if(key > root->_key)
		{
			return _EraseR(root->rightNode, key);
		}
		else
		{
			if (root->leftNode == nullptr)
			{
				Node* del = root;
				root = root->rightNode;
				delete del;
				return true;
			}
			else if(root->rightNode == nullptr)
			{
				Node* del = root;
				root = root->leftNode;
				delete del;
				return true;
			}
			else
			{
				/*方法一:*/
				//Node* swapNodeParent = root;
				//Node* swapNode = root->rightNode;
				//while (swapNode->leftNode)
				//{
				//	swapNodeParent = swap;
				//	swapNode = swapNode->leftNode;
				//}
				//std::swap(swapNode->_key, root->_key);
				//if (swapNodeParent->leftNode == swapNode)
				//{
				//	swapNodeParent->leftNode = swapNode->rightNode;
				//}
				//else
				//{
				//	swapNodeParent->rightNode = swapNode->rightNode;
				//}
				//delete swapNode;
				//return true;

				/*方法二:*/
				Node* swapNode = root->rightNode;
				while (swapNode->leftNode)
				{
					swapNode = swapNode->leftNode;
				}
				std::swap(swapNode->_key, root->_key);
				_EraseR(root->rightNode, key);
			}
		}
	}

代码实现逻辑:
查找删除节点其实和查找的逻辑一致,在这个只是添加了删除逻辑,和循环写的一样,还是要分为三个情况判断,这里值得去说就是方法二的巧妙之处,他是在与右子树的最左结带你交换之后,传入他的右子树去继续寻找那个与要删除结点交换值的节点,因为交换结点肯定是单个孩子或者是无孩子结点,可以通过之间的情况,利用他们的逻辑处理进行删除并返回。
在这里插入图片描述
这样的好处就是我们在删除交换结点的时候不用考虑他是他父结点的左孩子还是右孩子(针对交换结点还有右孩子的情况,需要把右孩子连接到交换结点的父节点去)

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值