【知识讲解】 二叉搜索树的相关知识与实现


目录

前言

[Part1. 二叉搜索树核心定义](#Part1. 二叉搜索树核心定义)

[Part1.1. 查找效率特性](#Part1.1. 查找效率特性)

[Part1.2. 不同查找容器对比](#Part1.2. 不同查找容器对比)

[Part1.3. 重复键值两种处理策略](#Part1.3. 重复键值两种处理策略)

[Part2. 二叉搜索树四大核心操作](#Part2. 二叉搜索树四大核心操作)

[Part2.1. 节点插入](#Part2.1. 节点插入)

[Part2.1. 中序遍历特性](#Part2.1. 中序遍历特性)

[Part2.2. 节点删除](#Part2.2. 节点删除)

[Part3. key/value二叉搜索树](#Part3. key/value二叉搜索树)

[Part4. 结语](#Part4. 结语)


前言

二叉搜索树作为二叉树的特化升级版,在查找上有着很大的优势,但同时也有着不小的问题,接下来让我们来看看吧。


let's go!!!!!!!!!


Part1. 二叉搜索树核心定义

二叉搜索树(Binary Search Tree,简称 BST)是满足特殊排序规则的二叉树:


规则<1>:节点左子树所有节点值 ≤ 当前根节点值。

规则<2>:节点右子树所有节点值 ≥ 当前根节点值。

规则<3>:左右子树本身也必须是二叉搜索树。


Part1.1. 查找效率特性

=》理想平衡 BST:查找次数等于树的高度 h,时间复杂度 O(logN)


=》极端不平衡 BST(链式退化):最坏退化成单链表,查找复杂度劣化为 O(N)。


优化方案:平衡二叉搜索树(AVL树、红黑树,(C++ STL 的 map/set 底层为红黑树)),多叉平衡树 B 树(企业级通常会有一千多个分支)。


Part1.2. 不同查找容器对比

除 BST 外,常用查找容器:

1. 哈希表:查询速度极快,O(1)级别。

2. 有序数组+二分查找:O(logN)查找,插入、删除操作效率极低,不适合频繁增删场景。


Part1.3. 重复键值两种处理策略

二叉搜索树对相同数值节点分两种实现方案,对应 STL 容器:

1. 去重模式:不允许插入重复值,对应 std::set / std::map

2. 允许重复:支持存储多个相同 key,对应 std::multiset / std::multimap


Part2. 二叉搜索树四大核心操作

完整实现需要封装四大接口:插入、查找、遍历、删除


Part2.1. 节点插入

节点的插入遵循二叉搜索树的规则来插入就好,接下来来看看代码吧:


cpp 复制代码
		bool Insert(const K& key)
		{
			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->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else//去重操作
				{
					return false;
				}
			}
			cur = new Node(key);
			if (parent->_key > key)
			{
				parent->_left = cur;
			}
			else
			{
				parent->_right = cur;
			}
			return true;
		}

Part2.1. 中序遍历特性

二叉搜索树中序遍历(左→根→右),结果严格升序;
升序顺序完全取决于插入数据的大小关系,与插入先后顺序无关。


cpp 复制代码
void InOrder()const//由于递归需要根节点 但是由于封装又不能暴露 于是就搞一个中介函数来作为其访问根节点的媒介
{
	_InOrder(_root);
	std::cout << std::endl;
}

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

Part2.2. 节点删除

删除是 BST 最难实现的接口,按待删除节点的子树存在情况分为 4 类,分别是:


<1>. 左右子树均为空。 <2>. 只有右子树,左子树为空。

<3>. 只有左子树,右子树为空。 <4>. 左右子树均不为空。

**p.s.**其中<1>可以归属与<2>或者<3>,就当成其有一个为nullptr的左或者右节点就好了。


对于<2>,我们使用parent指向待删除结点的父亲节点,cur则指向待删除结点,我们先判断cur是parent的左子还是右子,在让parent连接cur的右节点。(<3>同理)。


对于<4>,我们有两种方案(替换法):

=》方案A:取左子树最大值(左树最右节点),替换待删节点的值,再删除该最大值节点

=》方案B:取右子树最小值(右树最左节点),替换待删节点的值,再删除该最小值节点
我们来看看代码,同时看看其中的细节吧:


cpp 复制代码
		bool erase(const K& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key > key)//查找
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					if (cur->_left == nullptr)//无左节点场景
					{
						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
						}
						delete cur;
					}
					else if (cur->_right == nullptr)//无右节点场景
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}
						delete cur;
					}
					else
					{
						Node* repalce = cur->_right;
						Node* repalce_parent = cur;//将这个设为cur为根节点的场景
						while (repalce->_left != nullptr)
						{
							repalce_parent = repalce;
							repalce = repalce->_left;
						}
						cur->_key = repalce->_key;//替代
						if (repalce_parent->_left == repalce)//看看这个是左还是右 在连接
						{
							repalce_parent->_left = repalce->_right;
						}
						else
						{
							repalce_parent->_right = repalce->_right;
						}
						delete repalce;
					}
					return true;
				}
			}
			return false;
		}

Part3. key/value二叉搜索树

普通 BST 只存储单个数值,对应 set;而 map 采用 key-value 模型,适配业务中「唯一标识+附属数据」场景:

1. key:唯一排序标识
树的排序、查找、去重全部基于 key,key 不可修改,作为数据唯一区分依据。

2. value:附属业务数据
可自由修改,仅作为 key 附带的存储内容,不参与树的排序逻辑。

这个的实现与上述相似,只是多了一个value数值,我们来看看代码吧:

cpp 复制代码
namespace Key_Value
{
	template<class K,class V>
	struct BSTNode
	{
		K _key;
		V _value;
		BSTNode<K,V>* _left;
		BSTNode<K,V>* _right;

		BSTNode()
			:_key(K())
			,_value(V())
			, _left(nullptr)
			, _right(nullptr)
		{
		}

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

	template<class K,class V>
	class BSTree
	{
	public:
		using Node = BSTNode<K,V>;
		BSTree() = default;
		~BSTree()
		{
			Destroy(_root);
			_root = nullptr;
		}
		BSTree(const BSTree& Tree)
		{
			_root = Copy(Tree._root);
		}
		BSTree& operator=(BSTree tem)
		{
			std::swap(_root, tem._root);
			return *this;
		}
		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->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					return false;
				}
			}
			cur = new Node(key,value);
			if (parent->_key > key)
			{
				parent->_left = cur;
			}
			else
			{
				parent->_right = cur;
			}
			return true;
		}

		bool Find(const K& key)const
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else
				{
					return true;
				}
			}
			return false;
		}

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

		bool erase(const K& key)
		{
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
				{
					if (cur->_left == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
							}
							else
							{
								parent->_right = cur->_right;
							}
						}
						delete cur;
					}
					else if (cur->_right == nullptr)
					{
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
							}
							else
							{
								parent->_right = cur->_left;
							}
						}
						delete cur;
					}
					else
					{
						Node* repalce = cur->_right;
						Node* repalce_parent = cur;
						while (repalce->_left != nullptr)
						{
							repalce_parent = repalce;
							repalce = repalce->_left;
						}
						cur->_key = repalce->_key;
						if (repalce_parent->_left == repalce)
						{
							repalce_parent->_left = repalce->_right;
						}
						else
						{
							repalce_parent->_right = repalce->_right;
						}
						delete repalce;
					}
					return true;
				}
			}
			return false;
		}
	private:
		void _InOrder(Node* root)const
		{
			if (root == nullptr)
			{
				return;
			}
			_InOrder(root->_left);
			std::cout << root->_key << ":"<<root->_value<<" ";
			_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;
			}
			Node* new_node = new Node(root->_key);
			Copy(root->_left);
			Copy(root->_right);
			return new_node;
		}
		Node* _root = nullptr;
	};
}

Part4. 结语

二叉搜索树作为未来学习AVL数、红黑树的基石,了解它的删除、插入等的逻辑还是非常有必要的。
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~