数据结构 : 二叉搜索树的原理以及实现(C++)


二叉搜索树的概念

二叉搜索树又叫做二叉排序树,他是一个具有以下特性的二叉树。

1. 二叉搜索树的左孩子比父节点小,右孩子比父节点大。
2. 二叉搜索树的左子树的全部节点都小于根节点,右子树的全部节点都大于根节点。
3. 所有节点的左右子树都为二叉搜索树

4. 键值是唯一的,所以二叉搜索树不能有相同的键值。

例如以下数据

int arr[] = { 5, 3, 7, 9, 1, 4 };

按照上面的规则构建成为一个二叉搜索树
在这里插入图片描述
根据使用场景的不同,二叉搜索树还分为K模型和KV模型。
K模型:即只有key作为关键码,只需要存储Key即可,关键码即为需要搜索到的值。如STL中的set

KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。如STL中的map


二叉搜索树的作用

排序

中序遍历:先遍历左子树,再遍历根节点,再遍历右子树
而二叉搜索树的特性,左子树小于根节点,右子树大于根节点。
所以通过一趟中序遍历,即可获得排序的结果。


查找

对于二叉搜索树,查找是其主要的功能,STL中的map和set底层也是通过平衡二叉搜索树(红黑树)实现的。

二叉搜索树的查找十分简单,键值比根节点大则进入右子树,键值比根节点小则进入左子树,他的思路有点类似于二分查找,平均时间复杂度为O(log2N)

但是上述情况仅限于二叉搜索树为一个完全二叉树, 如果构建时树为有序数列,则二叉搜索树会退化为单支树,时间复杂度则会变为O(N)

扫描二维码关注公众号,回复: 11613702 查看本文章

如:

int arr[] = { 1, 3, 4, 5, 7, 9 };

在这里插入图片描述
如果要解决这个问题,就得为搜索二叉树加上平衡二叉树的属性,也就是我们通常所说的AVL树。


实现思路

查找

查找是二叉搜索树的核心,实现了这一部分后面的删除和插入也可以直接复用这里的部分代码

直接从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树,相同则返回。如果遍历完还没找到,则说明不存在此树中,返回nullptr

		Node* Find(const K& key)
		{
			//根据二叉搜索树的性质,从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树
			Node* cur = _root;

			while (cur)
			{
				//比根节点大则查找右子树
				if (key > cur->_key)
				{
					cur = cur->_right;
				}
				//比根节点小则查找左子树
				else if (key < cur->_key)
				{
					cur = cur->_left;
				}
				//相同则返回
				else
				{
					return cur;
				}
			}

			//遍历完则说明查找不到,返回false
			return nullptr;
		}

插入

插入前半段就可以复用查找的代码,我们首先要找到我们需要插入的位置(为了保证能够找到父节点还需要用一个指针指向父节点),找到了合适的位置后,我们需要判断当前的键值比父节点大还是比父节点小,来决定应该插入到父节点的左子树还是右子树

例如这里要插入8
在这里插入图片描述
查找到所在位置
在这里插入图片描述
判断是哪一个子树即完成插入
在这里插入图片描述

		bool Insert(const K& key)
		{
			//如果此时树为空,则初始化根节点
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}

			Node* cur = _root;
			Node* parent = cur;

			//找到合适的插入位置
			while (cur)
			{
				//比根节点大则查找右子树
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				//比根节点小则查找左子树
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				//相同则返回false,因为搜索树不能存在相同数据
				else
				{
					return false;
				}
			}

			cur = new Node(key);

			//判断cur要插入到parent的左子树还是右子树
			if (parent->_key > key)
			{
				parent->_left = cur;
			}
			else if (parent->_key < key)
			{
				parent->_right = cur;
			}

			return true;
		}

删除

相较于前面几个,删除会稍微复杂一点,因为对于非叶子节点,删除节点会导致搜索二叉树结构的破坏,所以不能直接删除,需要找到一个节点替换原有节点,再将原有节点删除。
删除分为三种情况。

1. 删除叶子节点
在这里插入图片描述
对于叶子节点1, 4, 9,我们可以直接的删除,不需要考虑别的。

2. 删除的节点只有一个子树
在这里插入图片描述
例如要删除这里的7,如果要保留原有节点,就需要让被删除节点的父节点,指向删除节点的唯一子树。
如:
在这里插入图片描述
3. 删除的节点有左右子树
这也是删除里最为麻烦的一块,例如这里的节点5。如果要删除5,就必须要找到一个能够替换5的节点来替换他,然后再将他删除,这样就不会破坏结构。而如何选择这个节点呢?如果要保持原来的结构,那么这个节点就必须要比左子树所有节点大,比右子树所有节点小,而符合的两个节点,则是左子树的最大节点,和右子树的最小节点。
也就是左子树的最右节点和右子树的最左节点。
这两个任意一个即可

首先我们要找到这个节点的位置,如这里选择的是4,我们就需要先找到这个4的位置。然后把4的值覆盖到被删除节点上。由于我们选择的是左子树的最右节点,所以这个节点必定没有右子树,但是不排除没有左子树。(这个图上没有,但是需要考虑有的情况,没有也没关系,反正接的是nullptr),所以需要借用到第二种情况的思路,把4的左子树接到父节点上后,再删除4.

再接着进一步分析,这里的第一二种情况可以合并处理,因为叶子节点的左右子树都为空,即使让父节点指向这两个空节点,也没有任何问题。
还有一种情况,如果删除的结点是根节点,则需要让删除节点的另一子树成为新的根节点。

		bool erase(const K& key)
		{
			/*
				删除有三种情况,一种是删除叶子节点,可以直接删除
				第二种情况,如果删除的节点只有一个子树,那么删除这个节点后,就让父节点指向他的这一子树
				前两种情况可以合并处理

				第三种情况则是左右子树都不为空,此时选择一个来节点来替换他后,再删除,就可以不破坏原有结构
				如果要保持原有结构不变化,那么选择的节点必须要和删除节点在中序遍历中是连续的,而满足的只有两个节点,一个是其左子树的最大值,一个是其右子树的最小值。
			*/

			Node* cur = _root;
			Node* parent = cur;

			while (cur)
			{
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					//前两种情况合并处理,如果当前结点只有一个子树,则让父节点指向他的子树
					//处理只有右子树时					
					if (cur->_left == nullptr)
					{
						//如果当前节点为根节点,则让右子树成为新的根节点
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							//判断当前节点是他父节点的哪一个子树
							if (parent->_right == cur)
							{
								parent->_right = cur->_right;
							}
							else
							{
								parent->_left = cur->_right;
							}
						}

						delete cur;
					}
					//处理只有左子树时	
					else if (cur->_right == nullptr)
					{
						//如果当前节点为根节点,则让左子树成为新的根节点
						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_right == cur)
							{
								parent->_right = cur->_left;
							}
							else
							{
								parent->_left = cur->_left;
							}
						}

						delete cur;
					}
					//处理左右子树都不为空时,选取左子树的最右节点或者右子树的最左节点
					else
					{
						//这里我选取的是左子树的最右节点

						Node* LeftMax = cur->_left;
						Node* LeftMaxParent = cur;

						//找到左子树的最右节点
						while (LeftMax->_right)
						{
							LeftMaxParent = LeftMax;
							LeftMax = LeftMax->_right;
						}

						//替换节点
						std::swap(cur->_kv, LeftMax->_kv);

						//判断当前节点是他父节点的哪一个子树, 因为已经是最右子树了,所以这个节点的右子树为空,但是左子树可能还有数据,所以让父节点指向他的左子树
						//并且删除最右节点
						if (LeftMax == LeftMaxParent->_left)
						{
							LeftMaxParent->_left = LeftMax->_left;
						}
						else
						{
							LeftMaxParent->_right = LeftMax->_left;
						}

						delete LeftMax;
					}

					return true;
				}

			}
			return false;
		}

删除、拷贝等

这几个和二叉树的删除拷贝一样,直接从根节点递归处理即可。


代码实现

K模型

#include<iostream>

namespace lee
{

	template<class K>
	class BSTreeNode
	{
	public:
		BSTreeNode(const K& key)
			: _left(nullptr)
			, _right(nullptr)
			, _key(key)
		{}

		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;
	};

	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
		typedef BSTree<K> Tree;

	public:
		BSTree() : _root(nullptr)
		{}

		~BSTree()
		{
			destory(_root);
		}

		BSTree(const Tree& temp) : _root(nullptr)
		{
			_root = copy(temp._root);
		}

		Tree& operator=(const Tree& temp)
		{
			if (this != &temp)
			{
				//先清空本树
				destory(_root);
				_root = copy(temp._root);
			}

			return *this;
		}

		//现代写法
		//Tree& operator=(Tree temp)
		//{
		//	swap(temp);

		//	return *this;
		//}

		void swap(Tree& temp)
		{
			std::swap(_root, temp._root);
		}

		//递归拷贝节点
		Node* copy(const Node* root)
		{
			if(!root)
				return nullptr;

			Node* temp = new Node(root->_key);
			temp->_left = copy(root->_left);
			temp->_right = copy(root->_right);

			return temp;
		}
		
		//递归销毁全部节点
		void destory(Node*& root)
		{
			Node* node = root;
			if (!root)
				return;

			destory(node->_left);
			destory(node->_right);

			delete node;
			node = nullptr;
		}

		bool Insert(const K& key)
		{
			//如果此时树为空,则初始化根节点
			if (_root == nullptr)
			{
				_root = new Node(key);
				return true;
			}

			Node* cur = _root;
			Node* parent = cur;

			//找到合适的插入位置
			while (cur)
			{
				//比根节点大则查找右子树
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				//比根节点小则查找左子树
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				//相同则返回false,因为搜索树不能存在相同数据
				else
				{
					return false;
				}
			}

			cur = new Node(key);

			//判断cur要插入到parent的左子树还是右子树
			if (parent->_key > key)
			{
				parent->_left = cur;
			}
			else if (parent->_key < key)
			{
				parent->_right = cur;
			}

			return true;
		}

		bool erase(const K& key)
		{
			/*
				删除有三种情况,一种是删除叶子节点,可以直接删除
				第二种情况,如果删除的节点只有一个子树,那么删除这个节点后,就让父节点指向他的这一子树
				前两种情况可以合并处理

				第三种情况则是左右子树都不为空,此时选择一个来节点来替换他后,再删除,就可以不破坏原有结构
				如果要保持原有结构不变化,那么选择的节点必须要和删除节点在中序遍历中是连续的,而满足的只有两个节点,一个是其左子树的最大值,一个是其右子树的最小值。
			*/

			Node* cur = _root;
			Node* parent = cur;

			while (cur)
			{
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					//前两种情况合并处理,如果当前结点只有一个子树,则让父节点指向他的子树
					//处理只有右子树时					
					if (cur->_left == nullptr)
					{
						//如果当前节点为根节点,则让右子树成为新的根节点
						if (cur == _root)
						{
							_root = cur->_left;
						}
						else
						{
							//判断当前节点是他父节点的哪一个子树
							if (parent->_right == cur)
							{
								parent->_right = cur->_right;
							}
							else
							{
								parent->_left = cur->_right;
							}
						}

						delete cur;
					}
					//处理只有左子树时	
					else if (cur->_right == nullptr)
					{
						//如果当前节点为根节点,则让左子树成为新的根节点
						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_right == cur)
							{
								parent->_right = cur->_left;
							}
							else
							{
								parent->_left = cur->_left;
							}
						}

						delete cur;
					}
					//处理左右子树都不为空时,选取左子树的最右节点或者右子树的最左节点
					else
					{
						//这里我选取的是左子树的最右节点

						Node* LeftMax = cur->_left;
						Node* LeftMaxParent = cur;

						//找到左子树的最右节点
						while (LeftMax->_right)
						{
							LeftMaxParent = LeftMax;
							LeftMax = LeftMax->_right;
						}

						//替换节点
						cur->_key = LeftMax->_key;

						//判断当前节点是他父节点的哪一个子树, 因为已经是最右子树了,所以这个节点的右子树为空,但是左子树可能还有数据,所以让父节点指向他的左子树
						//并且删除最右节点
						if (LeftMax == LeftMaxParent->_left)
						{
							LeftMaxParent->_left = LeftMax->_left;
						}
						else
						{
							LeftMaxParent->_right = LeftMax->_left;
						}

						delete LeftMax;
					}

					return true;
				}

			}
			return false;
		}

		void _InordTravel(Node* root)
		{
			if (root == nullptr)
				return;

			//先遍历左子树
			_InordTravel(root->_left);

			//遍历根节点
			std::cout << root->_key << std::ends;

			//遍历右子树
			_InordTravel(root->_right);
		}

		//提供给外界的接口,因为外界无法访问私有成员root
		void InordTravel()
		{
			_InordTravel(_root);
			std::cout << std::endl;
		}


		Node* Find(const K& key)
		{
			//根据二叉搜索树的性质,从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树
			Node* cur = _root;

			while (cur)
			{
				//比根节点大则查找右子树
				if (key > cur->_key)
				{
					cur = cur->_right;
				}
				//比根节点小则查找左子树
				else if (key < cur->_key)
				{
					cur = cur->_left;
				}
				//相同则返回
				else
				{
					return cur;
				}
			}

			//遍历完则说明查找不到,返回false
			return nullptr;
		}

	private:
		Node* _root;
	};
}


KV模型

#include<iostream>

namespace lee
{

	template<class K, class V>
	class BSTreeNode
	{
	public:
		BSTreeNode(const K& key = K(), const V& value = V())
			: _left(nullptr)
			, _right(nullptr)
			, _key(key)
			, _value(value)
		{}

		BSTreeNode<K, V>* _left;
		BSTreeNode<K, V>* _right;
		K _key;
		V _value;
	};

	template<class K, class V>
	class BSTree
	{
		typedef BSTreeNode<K, V> Node;
		typedef BSTree<K, V> Tree;

	public:
		BSTree() : _root(nullptr)
		{}

		~BSTree()
		{
			destory(_root);
		}

		BSTree(const Tree& temp) : _root(nullptr)
		{
			_root = copy(temp._root);
		}

		Tree& operator=(const Tree& temp)
		{
			if (this != &temp)
			{
				//先清空本树
				destory(_root);
				_root = copy(temp._root);
			}

			return *this;
		}

		//现代写法
		//Tree& operator=(Tree temp)
		//{
		//	swap(temp);

		//	return *this;
		//}

		void swap(Tree& temp)
		{
			std::swap(_root, temp._root);
		}

		Node* copy(const Node* root)
		{
			if (!root)
				return nullptr;

			Node* temp = new Node(root->_key, root->_value);
			temp->_left = copy(root->_left);
			temp->_right = copy(root->_right);

			return temp;
		}

		void destory(Node*& root)
		{
			Node* node = root;
			if (!root)
				return;

			destory(node->_left);
			destory(node->_right);

			delete node;
			node = nullptr;
		}

		bool Insert(const K& key, const V& value)
		{
			//如果此时树为空,则初始化根节点
			if (_root == nullptr)
			{
				_root = new Node(key, value);
				return true;
			}

			Node* cur = _root;
			Node* parent = cur;
			
			//找到合适的插入位置
			while (cur)
			{
				//比根节点大则查找右子树
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				//比根节点小则查找左子树
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				//相同则返回false,因为搜索树不能存在相同数据
				else
				{
					return false;
				}
			}

			cur = new Node(key, value);
			
			//判断cur要插入到parent的左子树还是右子树
			if (parent->_key > key)
			{
				parent->_left = cur;
			}
			else if (parent->_key < key)
			{
				parent->_right = cur;
			}

			return true;
		}

		bool erase(const K& key)
		{
			/*
				删除有三种情况,一种是删除叶子节点,可以直接删除
				第二种情况,如果删除的节点只有一个子树,那么删除这个节点后,就让父节点指向他的这一子树
				前两种情况可以合并处理

				第三种情况则是左右子树都不为空,此时选择一个来节点来替换他后,再删除,就可以不破坏原有结构
				如果要保持原有结构不变化,那么选择的节点必须要和删除节点在中序遍历中是连续的,而满足的只有两个节点,一个是其左子树的最大值,一个是其右子树的最小值。
			*/

			Node* cur = _root;
			Node* parent = cur;

			while (cur)
			{
				if (key > cur->_key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (key < cur->_key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					//前两种情况合并处理,如果当前结点只有一个子树,则让父节点指向他的子树
					//处理只有右子树时					
					if (cur->_left == nullptr)
					{
						//如果当前节点为根节点,则让右子树成为新的根节点
						if (cur == _root)
						{
							_root= cur->_left;
						}
						else
						{
							//判断当前节点是他父节点的哪一个子树
							if (parent->_right == cur)
							{
								parent->_right = cur->_right;
							}
							else
							{
								parent->_left = cur->_right;
							}
						}

						delete cur;
					}
					//处理只有左子树时	
					else if (cur->_right == nullptr)
					{
						//如果当前节点为根节点,则让左子树成为新的根节点
						if (cur == _root)
						{
							_root = cur->_right;
						}
						else
						{
							if (parent->_right == cur)
							{
								parent->_right = cur->_left;
							}
							else
							{
								parent->_left = cur->_left;
							}
						}

						delete cur;
					}
					//处理左右子树都不为空时,选取左子树的最右节点或者右子树的最左节点
					else
					{
						//这里我选取的是左子树的最右节点

						Node* LeftMax = cur->_left;
						Node* LeftMaxParent = cur;

						//找到左子树的最右节点
						while (LeftMax->_right)
						{
							LeftMaxParent = LeftMax;
							LeftMax = LeftMax->_right;
						}

						//替换节点
						std::swap(cur->_kv, LeftMax->_kv);

						//判断当前节点是他父节点的哪一个子树, 因为已经是最右子树了,所以这个节点的右子树为空,但是左子树可能还有数据,所以让父节点指向他的左子树
						//并且删除最右节点
						if (LeftMax == LeftMaxParent->_left)
						{
							LeftMaxParent->_left = LeftMax->_left;
						}
						else
						{
							LeftMaxParent->_right = LeftMax->_left;
						}

						delete LeftMax;
					}

					return true;
				}

			}
			return false;
		}

		void _InordTravel(Node* root)
		{
			if (root == nullptr)
				return;

			//先遍历左子树
			_InordTravel(root->_left);

			//遍历根节点
			std::cout << root->_key << ':' << root->_value << std::ends;

			//遍历右子树
			_InordTravel(root->_right);
		}

		//提供给外界的接口,因为外界无法访问私有成员root
		void InordTravel()
		{
			_InordTravel(_root);
			std::cout << std::endl;
		}

		
		Node* Find(const K& key)
		{
			//根据二叉搜索树的性质,从根节点出发,比根节点大则查找右子树,比根节点小则查找左子树
			Node* cur = _root;

			while (cur)
			{
				//比根节点大则查找右子树
				if (key > cur->_key)
				{
					cur = cur->_right;
				}
				//比根节点小则查找左子树
				else if (key < cur->_key)
				{
					cur = cur->_left;
				}
				//相同则返回
				else
				{
					return cur;
				}
			}

			//遍历完则说明查找不到,返回false
			return nullptr;
		}

	private:
		Node* _root;
	};
}

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/106766889