数据结构 --- 二叉搜索树

二叉搜索树

一、什么是二叉搜索树

二叉搜索树也叫做二叉排序树,是在二叉树的基础上增加了更多的限制条件产生的一种树,限制具体如下:

二叉搜索树整体呈现根节点的左子树小于此根节点的值,右子树大于此根节点的值,每一棵左右子树也遵循此规则,同时中可以⽀持插⼊相等的值,也可以不⽀持插⼊相等的值,但是一般不⽀持插⼊相等的值。

二叉搜索树举例:

二、二叉搜索树的简单实现

2.1整体结构

C++定义二叉搜索树,首先定义节点结构,使用struct定义公共节点类,将这个类定义为模板类,方便定义不同类型的二叉搜索树:

cpp 复制代码
// 定义根节点类
template<class K>
struct BSTNode
{
	K _key;
	BSTNode<K>* _left;
	BSTNode<K>* _right;

	// 构造节点
	BSTNode(const K& key)
		:_key(key)
		,_left(nullptr)
		,_right(nullptr)
	{}
};

随后使用节点来构造树tree。

cpp 复制代码
// 定义二叉搜索树类
template<class K>
class BSTree
{
	// 重命名BSTNode<K>类型
	typedef BSTNode<K> node;
	
public:
	// ..方法
	
private:
	// 属性 --- 节点
	node* _root = nullptr;
};

2.2插入

二叉搜索树的插入严格遵循对于每一个根节点,其左子树都小于根节点,右子树都大于根节点:
注意:规定本树不可以插入相同的值。

cpp 复制代码
	// 插入
	/*
	* 调用方法传key
	* (1)若二叉搜索树为空(nullptr),则直接通过key创建一个根节点
	* (2)先找到合适的插入位置,创建cur和parent,锁定插入位置和插入位置的父节点
	* (3)插入的key比插入位置的父节点的key小,则作为左孩子链接
	*	  插入的key比插入位置的父节点的key大,则作为右孩子链接
	*/
	bool Insert(const K& key)
	{
		// 一开始树为空,直接将key插入作为根节点
		if (_root == nullptr)
		{
			_root = new node(key);
			return true;
		}

		// 找到待插入key值的位置
		node* parent = nullptr;
		node* cur = _root;
		while (cur)
		{
			// 比key小,向下找左子树
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			// 比key大,向下找右孩子
			else if(key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			// 不允许插入相等key值
			else
			{
				return false;
			}
		}

		// while运行完毕,代表找到了合适的位置
		// 此时cur指向nullptr
		cur = new node(key);

		// 插入key比找到的位置的前一个节点的key小,则链接为左孩子
		if (key < parent->_key)
		{
			parent->_left = cur;
		}
		// 插入key比找到的位置的前一个节点的key大,则链接为右孩子
		else if(key > parent->_key)
		{
			parent->_right = cur;
		}

		// 成功的插入返回true
		return true;
	}

构造一棵二叉搜索树:

cpp 复制代码
BSTree<int> tree;
int arr[] = { 2,4,7,9,1,5,6,8,3,10 };

for (auto e : arr)
{
	tree.Insert(e);
}

构造出来的树为:

2.3查找

查找的逻辑非常简单,首先找到这个节点,返回true/false即可,找节点的算法和插入节点中找到合适位置的插入算法是一模一样的:

cpp 复制代码
// 查找
bool Find(const K& key)
{
	// 找到key值的位置
	node* parent = nullptr;
	node* cur = _root;
	while (cur)
	{
		// 比key小,向下找左子树
		if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		// 比key大,向下找右孩子
		else if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		// key值相等,代表找到了,和插入方法中的唯一区别
		else
		{
			return true;
		}
	}
	return false;
}

2.4删除

对于节点的删除是二叉搜索树中最麻烦的一个算法,分为以下五种情况:

(1)当删除节点是叶子节点的时候,直接delete掉即可

(2)当删除节点是当前节点的父节点的左孩子时,且删除节点的左孩子为空(代表右孩子存在),则需要将父节点的左链接到删除节点的右

(3)当删除节点是当前节点的父节点的右孩子时,且删除节点的右孩子为空(代表左孩子存在),则需要将父节点的右链接到删除节点的左。

(4)当删除节点的左右孩子都存在,则使用向下替换节点进行间接删除:

在情况4中又有两种方法可以选择,向下找左子树最大节点,或者向下找右子树最小节点,原因还是二叉搜索树的特点,根节点的左子树永远比根节点小,根节点的右子树永远比根节点大,找到之后进行交换,以保证删除节点时整棵树的结构不会发生破坏。

(5)当删除节点是根节点时,直接让删除节点的左孩子或者右孩子作为根节点即可。

上述第一点可以和第二点/第三点合并,只不过父节点左/右指向成了nullptr。

cpp 复制代码
// 删除 --- 核心思想是向下找替换key
// 替换的原则是:找需要删除节点的左子树最大key,或者找右子树最小key
bool Erase(const K& key)
{
	if (_root == nullptr)
	{
		return false;
	}

	// 也是先找到key值的位置
	node* parent = nullptr;
	node* cur = _root;
	while (cur)
	{
		// 比key小,向下找左子树
		if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		// 比key大,向下找右孩子
		else if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		// key值相等,代表找到了,准备删除
		else
		{
			// 若待删除节点左孩子为空,则表明存在右孩子
			if (cur->_left == nullptr)
			{
				// 如果删除节点不是根节点
				if (cur != _root)
				{
					// 待删除节点是其父节点的左孩子,则让父节点的左链接删除节点的右
					if (cur == parent->_left)
					{
						parent->_left = cur->_right;
					}
					// 待删除节点是其父节点的右孩子,则让父节点的右链接删除节点的右
					else if (cur == parent->_right)
					{
						parent->_right = cur->_right;
					}

					delete cur;
				}
				// 删除节点是根节点,则直接让删除节点的右成为根节点
				else
				{
					_root = cur->_right;
				}
			}
			// 若待删除节点右孩子为空,则表明存在左孩子
			else if (cur->_right == nullptr)
			{
				if (cur != _root)
				{
					// 待删除节点是其父节点的左孩子,则让父节点的左链接删除节点的左
					if (cur == parent->_left)
					{
						parent->_left = cur->_left;
					}
					// 待删除节点是其父节点的右孩子,则让父节点的右链接删除节点的左
					else if (cur == parent->_right)
					{
						parent->_right = cur->_left;
					}

					delete cur;
				}
				else
				{
					_root = cur->_left;
				}
				
			}
			// 若待删除节点左右孩子均有,则向下找可替换key
			else
			{
				// 这里找右子树最小key
				node* minRightParent = cur;
				node* minRight = cur->_right;
				while (minRight->_left)
				{
					minRightParent = minRight;
					minRight = minRight->_left;
				}

				// 找到了最小的key
				std::swap(minRight->_key, cur->_key);

				// 链接逻辑和上面if,elseif相同
				if (minRight == minRightParent->_left)
				{
					minRightParent->_left = minRight->_left;
				}
				else if (minRight == minRightParent->_right)
				{
					minRightParent->_right = minRight->_left;
				}

				delete minRight;
			}
			return true;
		}
	}
	return false;
}

2.5遍历

二叉搜索树的遍历使用中序遍历方式,二叉搜索树的中序遍历结果是升序序列,所以说中序遍历同时也兼顾了排序的功能。

中序遍历的思想就是递归,依次按照每一棵树的左根右顺序进行遍历:

cpp 复制代码
	// 类中封装,类外直接.InOrder()访问
	void InOrder()
	{
		inorder(_root);
		std::cout << std::endl;
	}

private:
	// 遍历 --- 使用中序遍历,最终是升序序列
	// 中序 --- 左根右
	void inorder(const node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		inorder(root->_left);
		std::cout << root->_key << "  ";
		inorder(root->_right);
	}

方法演示:

cpp 复制代码
int main()
{
	BSTree<int> tree;
	int arr[] = { 2,4,7,9,1,5,6,8,3,10 };

	for (auto e : arr)
	{
		tree.Insert(e);
	}
	tree.InOrder();

	if (tree.Find(10))
	{
		std::cout << "找到了!!" << std::endl;
	}
	else
	{
		std::cout << "没找到!!" << std::endl;
	}

	tree.Erase(3);
	tree.InOrder();

	tree.Erase(1);
	tree.InOrder();

	tree.Erase(9);
	tree.InOrder();

	tree.Erase(2);
	tree.InOrder();

	return 0;
}

中序遍历,查找,删除总的运行结果:

三、用途

单一的二叉搜索树是没有意义的,我们无法控制节点的插入,会导致一个现象叫做"未平衡",也就是最坏的情况会存在单支二叉搜索树,这是时间复杂度就由O(logN)变为O(N),时间复杂度大大增加,所以使其保持"平衡"就非常重要,平衡二叉搜索树有一个新的名字叫做AVL树,引入更加复杂的修复(平衡操作),就变成了红黑树。

随后学习的STL容器map/set的底层都是红黑树。

相关推荐
浪漫血液&44 分钟前
索引为什么用B+树而不是B树
数据结构·数据库·b树·b+树
Wild_Pointer.1 小时前
高效工具实战指南:CMake构建工具
c++·软件构建
可峰科技1 小时前
Apriltag_ros CMakeList.txt一句话导致其他包编译失败
c++
code bean1 小时前
【C++ 】C++ 与 C#:using 关键字、命名空间及作用域解析符对比
开发语言·c++·c#
CAE虚拟与现实1 小时前
C# 调用 DLL为什么不像 C/C++调用 DLL 时需要lib库
开发语言·c++·c#·动态链接库·dll库·lib库
Larry_Yanan1 小时前
Qt线程使用(一)直接继承QThread类
开发语言·c++·qt·ui
Ayanami_Reii1 小时前
基础数据结构应用-一个简单的整数问题
数据结构·算法·树状数组·fenwick tree
Yu_Lijing1 小时前
【个人项目】C++基于websocket的多用户网页五子棋(上)
开发语言·c++·websocket
Ayanami_Reii1 小时前
进阶数据结构应用-一个简单的整数问题2(Fenwick-Tree 解法)
数据结构·算法·前缀和·差分·树状数组·fenwick tree