【STL】二叉搜索树 BSTree(底层讲解 + key_value(KV)的引入)

目录

二叉搜索树简介

二叉搜索树结构构建

Insert 插入

Find 查找

erase 删除

key_value的引入

结语


二叉搜索树简介

二叉搜索树和普通二叉树的区别就在于:

二叉搜索树规定:左子树必须比根小,右子树比如比根大(如果相等就不插入该数据)

由这一个特性我们就能推出很多东西:

  1. 中序遍历二叉搜索树是从小到大的有序序列(中序就是左、根、右子树,刚好左<根<右,所以中序就是从小到大)
  2. 可以用于去重,因为相同的数据并不会重复插入
  3. 用来查找数据效率很高(一般情况下)

如上是一棵典型的二叉搜索树

如果我们要给二叉搜索树起名的话,我们需要注意一个点,二叉的英文是binary,搜索是search

我们叫二叉搜索就是BS,叫搜索二叉就是。。

所以我们尽量叫二叉搜索树,免得引起不必要的麻烦

二叉搜索树结构构建

我们二叉搜索树的成员就是一个节点类型,所以我们需要构建出一个节点的类,就像list的实现一样,如下:

template<class k>
struct BSTNode
{
	k _key;
	BSTNode<k>* _left;
	BSTNode<k>* _right;

	BSTNode(const k& key)
		:_key(key)
		,_left(nullptr)
		, _right(nullptr)
	{}
};

然后我们再创建一个类,这个类就是搜索二叉树的类了,并且这个类的成员变量只有一个,就是节点的指针,这个指针指向的就是头节点(root)

如下:

template<class k>
class BSTree
{
	typedef BSTNode<k> Node;
public:

	//二叉搜索树的接口函数

private:

	Node* _root = nullptr;
};

Insert 插入

其实插入很简单的,我们也不用写递归,因为直接用循环更简单,逻辑更清晰一些

我们只需要设置一个cur节点,节点初始化为_root(根),我们从根开始往下找,并且我们的循环条件是cur为空,只要cur为空,就代表找到待插入的位置了

但是我们找到了cur并没有用,我们需要将其与搜索二叉树链接起来,所以我们还需要定义一个parent节点,时刻记录着cur节点的父节点位置

如果待插入的值比过该节点的值要小,那么我们就往左子树走

如果待插入的值比过该节点的值要小,那么我们就往右子树走

如果我们能找得到一样的值,那我们就直接return false,因为都找得到一样的值了还插入什么

接着我们退出了循环之后,就代表cur为空,cur节点的位置就是待插入的位置

我们new了一个节点之后,就直接将其与父节点链接起来即可

代码如下:

bool insert(const k& key)
{
	if (_root == nullptr)
	{
		_root = new Node(key);
		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);
	if (parent->_key < key)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	return true;

}

Find 查找

查找的逻辑就是,先定义一个cur节点,将其初始化为_root(根)

接着就是插入前面的逻辑了,这里就不再赘述了,代码如下:

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;
}

erase 删除

删除会相对比较难一些

在删除之前,我们得先找到该节点,也是定义一个cur节点,然后初始化为根,要找的值小于当前节点就找左子树,反之就是右子树,直到我们找到了,这时候才进行一个删除逻辑,如果没找到的话,就直接return false,都找不到还删什么

目前代码如下:

bool erase(const k& key)
{
	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;
}

接下来我们就来讲一讲删除的主逻辑:
首先我们需要分情况讨论:

  1. 待删除节点没有孩子
  2. 待删除节点有一个孩子
  3. 待删除节点有两个孩子

为什么要分成这三种情况呢?因为:

①如果没有孩子的话,直接删除即可,不用管那么多

②如果有一个孩子的话,我们就需要将这个孩子与待删除节点的父节点链接起来

③如果有两个孩子的话,我们就需要找待删除节点左子树的最右节点、或者右子树的最左节点

因为如果有两个孩子的话,我们需要将该节点换走,换成左子树的最右节点、或者右子树的最左节点,只有这两个节点满足比左子树的整体大,比右子树的整体小

然后我们将两个节点的值一交换,然后就直接把右子树的最左(假设选这个)给删掉

这时我们可以确认的是,这个节点一定满足情况①或②,因为是最左或最右了,肯定只有一个孩子或者没有孩子,所以删完了之后,直接走①或②的逻辑即可

// 没有孩子 and 只有一个孩子
// 有两个孩子
if (cur->_left == nullptr)
{
	if (parent == nullptr)
	{
		_root = cur->_right;
	}
	else
	{
		if (cur == parent->_left)
			parent->_left = cur->_right;
		else
			parent->_right = cur->_right;
	}
	delete cur;
	return true;
}
else if (cur->_right == nullptr)
{
	if (parent == nullptr)
	{
		_root = cur->_left;
	}
	else
	{
		if (cur == parent->_left)
			parent->_left = cur->_left;
		else
			parent->_right = cur->_left;
	}
	delete cur;
	return true;
}
else
{
	// 这是有两个孩子的情况
	// 我们就需要找左的最右 或 右的最左√
	Node* rightMinP = cur;
	Node* rightMin = cur->_right;

	while (rightMin->_left)
	{
		rightMinP = rightMin;
		rightMin = rightMin->_left;
	}

	cur->_key = rightMin->_key;

	if (rightMinP->_right == rightMin)
	    rightMinP->_right = rightMin->_right;
	else
	    rightMinP->_left = rightMin->_right;

	delete rightMin;
	return true;
}

需要注意的是,我们的中间还有一段关于parent节点是否为空的判断

这是因为,如果删除的节点是根的话,这时候如果刚好根的左子树或右子树为空,这段代码就派得上用场

而后面两个孩子的情况,还分类讨论说rightMin是rightMinP的左还是右

我们来看这样一种情况:

如果待删除的节点是6

因为有一种特殊情况就是,如果待删除节点的右节点,就是右节点的最左节点(该节点左节点为空),所以我们需要判断一下

key_value的引入

key_value是什么呢,其实就是我们通过一个key,就能找到对应的value

比如我们生活中很常见的英文字典,我们通过找一个英文,就能找到对应的中文解释

再比如车库停车系统(key为车牌,value为状态判断),一辆车过来了,通过扫描就能得知这个车牌有没有录入系统里面

我们今天来初步将key_value与二叉搜索树结合一下,因为二叉搜索树学完之后就会进入AVL树与红黑树的学习,这两个都可以拿来封装set和map(一般红黑树用得多)

set就是我们平常的找一个值在不在数据结构里面

map就是通过一个key找到数据结构中对应的value

——————分割线——————

首先,我们的模板那里肯定得改一改,节点类中需要多加一个成员变量value

然后,我们的插入也需要更改,我们还是拿key来找节点在那个位置,但是参数需要多传一个value,这样我们在new节点的时候,就将value传进去构建节点

然后我们的find并不需要修改,因为他只需要用key来查找即可

我们的erase还是需要改变的,因为我们是拿右子树的最左或左子树的最右节点进行替换,所以我们的key和value都要进行替换,不然就会变成这样:

代码如下:

namespace key_value
{
	template<class k, class v>
	struct BSTNode
	{
		k _key;
		v _value;
		BSTNode<k, v>* _left;
		BSTNode<k, v>* _right;

		BSTNode(const k& key, const v& value)
			:_key(key)
			,_value(value)
			, _left(nullptr)
			, _right(nullptr)
		{}
	};

	template<class k, class v>
	class BSTree
	{
		typedef BSTNode<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;

		}

		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;
		}

		bool erase(const k& key)
		{
			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
				{
					// 没有孩子 and 只有一个孩子
					// 有两个孩子
					if (cur->_left == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->_right;
						}
						else
						{
							if (cur == parent->_left)
								parent->_left = cur->_right;
							else
								parent->_right = cur->_right;
						}
						delete cur;
						return true;
					}
					else if (cur->_right == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->_left;
						}
						else
						{
							if (cur == parent->_left)
								parent->_left = cur->_left;
							else
								parent->_right = cur->_left;
						}
						delete cur;
						return true;
					}
					else
					{
						// 这是有两个孩子的情况
						// 我们就需要找左的最右 或 右的最左√
						Node* rightMinP = cur;
						Node* rightMin = cur->_right;

						while (rightMin->_left)
						{
							rightMinP = rightMin;
							rightMin = rightMin->_left;
						}

						cur->_key = rightMin->_key;
						cur->_value = rightMin->_value;

						if (rightMinP->_right == rightMin)
							rightMinP->_right = rightMin->_right;
						else
							rightMinP->_left = rightMin->_right;

						delete rightMin;
						return true;
					}
				}
			}
			return false;
		}

		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);
		}

		Node* _root = nullptr;
	};

}

测试代码如下:

namespace key_value
{
	void test_key_value_bstree()
	{
		BSTree<string, string> bs;

		bs.insert("left", "左边");
		bs.insert("right", "右边");
		bs.insert("word", "单词");
		bs.insert("I love you", "我喜欢你");
		bs.insert("I hate you", "我讨厌你");

		bs.InOrder();

		bs.erase("left");
		bs.InOrder();
		bs.erase("right");
		bs.InOrder();
		bs.erase("word");
		bs.InOrder();
		bs.erase("I love you");
		bs.InOrder();
		bs.erase("I hate you");

	}
}

int main()
{
	key_value::test_key_value_bstree();
	return 0;
}

结语

看到这里,这篇博客有关二叉搜索树的相关内容就讲完啦~( ̄▽ ̄)~*

如果觉得对你有帮助的话,希望可以多多支持博主喔(○` 3′○)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值