STL库——二叉搜索树

发布于:2025-09-09 ⋅ 阅读:(20) ⋅ 点赞:(0)

ʕ • ᴥ • ʔ

づ♡ど

 🎉 欢迎点赞支持🎉

个人主页:励志不掉头发的内向程序员

专栏主页:C++语言



前言

这一章节我们来聊聊我们之前的二叉树的一种新形态二叉搜索树,这个形态也有一定的问题,但是它是我们后面讲解 map/set 容器的基础,我们到后面才会讲解改怎么解决二叉搜索树的方式,但是我们本章节搞明白二叉搜索树的原理和实现也非常重要。


一、二叉搜索树的概念

二叉搜索树又称二叉排序树,它要么是一颗空树,要么是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值。
  • 若它的右子树不为空,则右子树上所有结点的值都大于等于根节点的值。
  • 它的左右子树也分别为二叉搜索树。
  • 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习 map/set/multimap/multiset 系列容器底层就是二叉搜索树,其中 map/set 不支持插入相等值,multimap/multiset 支持插入相等值。

我们二叉树的逻辑就是,当插入一个新结点时,从头结点,也就是树的根结点开始做比较,如果比新结点大,就走到我们的右结点。如果比新小节点小,就走到我们的左节点,然后再和这些结点进行比较,一直到结点走到底,没有结点比较时就插入即可。

我们学习数据结构的时候可能有听说过我们二叉树如果单独存储数据是没有意义的。要想有意义,我们得在二叉树的基础上加入一些东西,这样才有意义,就比如在二叉树的基础上加入搜索二叉树的规则。

二、二叉搜索树的性能分析

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:log2(N)。

最差情况下,二叉树退化为单支树(或者类似单支),其高度为:N。

所以综合而言二叉搜索树增删查改时间复杂度为:O(N)。

这样的效率显然是无法满足我们需求的,我们后面章节会继续讲解二叉搜索树的变形,平衡二叉搜索树AVL树和红黑树,才能适用于我们在内存中存储和搜索数据。

另外,我们二分查找也可以实现 O( log2(N) ) 级别的查找效率,但是二分查找有两大缺陷:

  • 需要存储在支持下标随机访问的结构中,并且有序。
  • 插入和删除数据效率很低,因为存储在下标随机访问的数据结构中,插入和删除数据一般需要挪动数据。

三、二叉搜索树的插入

插入的具体过程如下:

  • 树为空,则直接新增结点,赋值给 root 指针。
  • 树不为空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
  • 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点(要注意的是要保持逻辑一致性,插入相等的值不要一会儿往左走,一会儿往右走)。

就如上图一样的插入方式。

我们先把二叉搜索树的框架搭建出来,创建一个结点类,这个结点包含左结点的指针和右结点的指针,以及一个要保存的类型。

namespace zxl
{
	template <class K>
	struct BSTNode
	{
		BSTNode<K>* _left;
		BSTNode<K>* _right;
		K _key;

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

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


	private:
		Node* _root = nullptr;
	};
}

我们设计一个插入的成员函数,它的返回值可以是布尔类型来查看是否插入成功。我们可以通过while 循环比较的方式一直比较到 nullptr 时让循环停下来,但是此时我们的 cur 指向的是一个空指针,我们应该指向它的上一个指针,所以我们还得创建一个变量来记录上一个变量,通过 cur 的上一个变量来插入我们的新结点。我们这里实现的是不允许有相等的情况出现的,如果想要实现有相等的情况可以在比较那里改一下即可。

我们可以来看看下面的动画来帮助理解。

bool insert(const T& 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;
}

此时我们可以写一个中序遍历来看看我们实现的怎么样,由于我们的二叉搜索树的结构,这导致我们的中序遍历出来的是一个从小到大排序好的数据,这样就可以验证我们的插入实现的是否成功。

我们把中序遍历写在私有位置,这样就可以不把根结点暴露出去。我们想要调用递归,就必须传Node 的指针,如果我们把我们的中序遍历写在公有,那它有默认的 this 指针,可以调用 _root,但是此时去下一层就得传下一层的地址,但是由于我们之前是靠 this 指针而得到的 _root。所以此时我们就无法传值到递归下一层。但是如果我们想要写一个传值的成员函数,不写在私有的话因为要传指针所以就得暴露我们的 _root。所以为了避免我们干脆直接写成私有的。

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

此时我们就有一个问题,那就是我们在外界没办法调用我们的中序遍历,此时我们可以在类内公有位置创建一个成员函数,让这个成员函数去调用我们的中序遍历即可。

public:
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

此时我们就可以尝试去测试我们的插入了。

int main()
{
	zxl::BSTree<int> t;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };

	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		t.insert(a[i]);
	}

	t.InOrder();
	return 0;
}

四、二叉搜索树的查找

我们查找的逻辑就和搜索的逻辑一样。

  • 从根开始比较,查找 x,x 比根的值大则往右边查找,x 比根小则往左边查找。
  • 最多查找高度次,走到空还没找到这个值就不存在。
  • 如果不支持插入相等的值,找到 x 即可返回。
  • 如果支持插入相等的值,意味着有多个 x 存在,一般要求查找中序的第一个 x。
bool Find(const T& 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;
}

五、二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回 false。

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为 N)

  1. 要删除结点 N 左右孩子结点均为空。
  2. 要删除结点 N 左孩子结点为空,右孩子结点不为空。
  3. 要删除结点 N 右孩子结点为空,左孩子结点不为空。
  4. 要删除结点 N 左右孩子结点均不为空。

第一种情况:

第二种情况:

第三种情况:

第四种情况:

对应以上四种情况的解决方案:

  1. 由于 N 结点左右孩子都是空,所以我们直接销毁掉我们的这个结点即可,但是为了让它的父亲结点不会指向野指针,我们在销毁这个结点后要记得把指向它的父亲结点指向 nullptr。
  2. 把N结点的父亲对应孩子指针指向N的右孩子,然后就像 1 一样删除掉 N 结点即可。
  3. 把N结点的父亲对应孩子指针指向N的左孩子,然后就像 1 一样删除掉 N 结点即可。
  4. 无法直接删除 N 结点,因为 N 的两个孩子无处安放,只能用替换法删除。找 N 左子树的值最大结点 R(最右结点)或者 N 右子树的值最小结点 L(最左结点)代替 N,因为这两个结点中任意一个,放到 N 的位置,都满足二叉搜索树的规则。替代 N 的意思就是 N 和 R 的两个结点的值的交换,转而变成删除 R 结点,R 结点符号情况 2 或 情况 3,可以直接删除。

我们先用循环去找到我们的 key 值,逻辑和上面一样,当我们的找到我们 key 时,我们就开始讨论这 4 种情况,我们的情况 1 其实可以融合在情况 2 或 3中,所以这里就只讨论 3 种情况。

第一种情况:

第二种情况:

bool Erase(const T& 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
		{
			// 0-1个孩⼦的情况
			// 删除情况1 2 3均可以直接删除,改变⽗亲对应孩⼦指针指向即可
			if (cur->_left == nullptr)
			{
				if (parent == nullptr)
				{
					_root = cur->_right;
				}
				else
				{
					if (parent->_left == cur)
						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 (parent->_left == cur)
						parent->_left = cur->_left;
					else
						parent->_right = cur->_left;
				}
				delete cur;
				return true;
			}
			else
			{
				// 2个孩⼦的情况
				// 删除情况4,替换法删除
				// 假设这⾥我们取右⼦树的最⼩结点作为替代结点去删除
				// 这⾥尤其要注意右⼦树的根就是最⼩情况的情况的处理,
                // 对应课件图中删除7的情况
				// ⼀定要把cur给rightMinP,否会报错。
				Node* rightMinP = cur;
				Node* rightMin = cur->_right;
				while (rightMin->_left)
				{
					rightMinP = rightMin;
					rightMin = rightMin->_left;
				}
				cur->_key = rightMin->_key;
				if (rightMinP->_left == rightMin)
					rightMinP->_left = rightMin->_right;
				else
					rightMinP->_right = rightMin->_right;
				delete rightMin;
				return true;
			}
		}
	}
	return false;
}

六、二叉搜索树的析构

我们二叉搜索树的析构可以使用递归,向我们的中序遍历一样得让递归函数私有,然后再在公有的地方实现析构函数以此来保护我们的根结点。

public:
	~BSTree()
	{
		Destroy(_root);
		_root = nullptr;
	}

private:
	void Destroy(Node* root)
	{
		if (root == nullptr)
			return;
		Destroy(root->_left);
		Destroy(root->_right);
		delete root;
	}

七、二叉搜索树的拷贝

和我们的析构同理,也是用递归来实现。

public:
	BSTree(const BSTree<K, V>& t)
	{
		_root = Copy(t._root);
	}

private:
	Node* Copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;
		Node* newRoot = new Node(root->_key, root->_value);
		newRoot->_left = Copy(root->_left);
		newRoot->_right = Copy(root->_right);
		return newRoot;
	}

剩下的像默认构造没有必要实现,而赋值重载用我们的交换大法即可轻松完成,这里就不过多赘述。

八、二叉搜索树 key 和 key/value 使用场景

6.1、key搜索场景

只有 key 作为关键码,结构中只需要存储 key 即可,关键码即为需要搜索到的值,搜索场景只需要判断 key 在不在。key 的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改 key 破坏搜索树机构了。

场景1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则体现非本小区车辆,无法进入。

场景2:检查一篇英文文档拼写是否正确,将词库中所有的单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示。

6.2、key/value 搜索场景:

每⼀个关键码 key,都有与之对应的值 value,value 可以任意类型对象。树的结构中 (结点) 除了需要存储 key 还要存储对应的 value,增/删/查还是以 key 为关键字⾛⼆叉搜索树的规则进行比较,可以快速查找到 key 对应的 value。key/value 的搜索场景实现的⼆叉树搜索树⽀持修改,但是不⽀持修改 key,修改 key 破坏搜索树性质了,可以修改 value。

场景1:简单中英互译字典,树的结构中 (结点) 存储 key (英⽂) 和 vlaue (中⽂),搜索时输入英⽂,则同时查找到了英⽂对应的中⽂。

场景2:商场无人值守车库,入口进场时扫描车牌,记录⻋牌和入场时间,出口离场时,扫描车牌,查找入场时间,⽤当前时间 - 入场时间计算出停车时长,计算出停⻋费⽤,缴费后抬杆,车辆离场。

场景3:统计⼀篇⽂章中单词出现的次数,读取⼀个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数。

6.3、key/value 二叉搜索树代码实现

我们 key/value 的结构和我们上面的 key 结构相比,无非就是多了一个 value,我们在结点创建时多创建一个 value,然后在增删查的时候我们只要让我们传过来的值中的 key 进行比较,然后 value 不管,插入时把 key/value 都插入即可实现,我们可以通过下面的 key/value 的代码和上面的 key 作比较就能看出其中的相似。

namespace zxl
{
	template<class K, class V>
	struct BSTNode
	{
		// pair<K, V> _kv;
		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:
		BSTree() = default;
		BSTree(const BSTree<K, V>& t)
		{
			_root = Copy(t._root);
		}
		BSTree<K, V>& operator=(BSTree<K, V> t)
		{
			swap(_root, t._root);
			return *this;
		}
		~BSTree()
		{
			Destroy(_root);
			_root = nullptr;
		}
		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;
		}
		Node* 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 cur;
				}
			}
			return nullptr;
		}
		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
				{
					if (cur->_left == nullptr)
					{
						if (parent == nullptr)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
								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 (parent->_left == cur)
								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->_left == rightMin)
							rightMinP->_left = rightMin->_right;
						else
							rightMinP->_right = 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);
		}
		void Destroy(Node* root)
		{
			if (root == nullptr)
				return;
			Destroy(root->_left);
			Destroy(root->_right);
			delete root;
		}
		Node* Copy(Node* root)
		{
			if (root == nullptr)
				return nullptr;
			Node* newRoot = new Node(root->_key, root->_value);
			newRoot->_left = Copy(root->_left);
			newRoot->_right = Copy(root->_right);
			return newRoot;
		}
	private:
		Node* _root = nullptr;
	};

}


总结

以上便是我们二叉搜索树的概念和实现了,二叉搜索树是一个非常厉害的思想,但是它却还是有一些弊端,那就是有的时候没有办法形成一个完美的完全搜索二叉树,为了解决这个问题,我们后面会讲解决的办法,所以我们一定要把这一章节搞明白。

🎇坚持到这里已经很厉害啦,辛苦啦🎇

ʕ • ᴥ • ʔ

づ♡ど


网站公告

今日签到

点亮在社区的每一天
去签到