【数据结构】二叉搜索树

✨✨欢迎大家来到Celia的博客✨✨

🎉🎉创作不易,请点赞关注,多多支持哦🎉🎉

所属专栏:数据结构

个人主页Celia's blog~

目录

 一、二叉搜索树

1.1 二叉搜索树的定义

1.2 为什么要用二叉搜索树

二、实现二叉搜索树

2.1 节点结构定义

2.2 插入函数Insert

2.3 查找函数Find

2.4 删除函数Enrase

2.4.1 特殊情况处理

 

2.5 遍历二叉搜索树

三、源码


 一、二叉搜索树

1.1 二叉搜索树的定义

  二叉搜索树是一颗二叉树,但是这颗二叉树的左右节点具有一定的要求:左子树所有的值要小于根节点的值,右子树所有的值要大于根节点的值。这样一来,当查找一个值的时候,如果比当前节点的值小,就向左遍历;如果比当前节点的值大,就向右遍历。这样的搜索模式最好的时间复杂度为logN。

二叉搜索树举例

1.2 为什么要用二叉搜索树

  二叉搜索树的遍历方式类似于二分查找,但是二分查找的应用有三个致命的缺点:

  1. 存储数据的底层结构必须是数组。
  2. 如果有插入、删除等操作,数组需要大量挪动数据。
  3. 数组元素必须有序。

想要弥补这些缺点,就可以用到二叉搜索树,解决了存储结构的限制,插入、删除操作相对便捷,插入每一个元素时,按照一定的规则插入即可。

二、实现二叉搜索树

2.1 节点结构定义

template<class K>
class BSTNode    //每个节点的定义
{
public:
	K val;
	BSTNode<K>* left;
	BSTNode<K>* right;
	BSTNode(const K& val)
		:val(val)
		,left(nullptr)
		,right(nullptr){}
};

template<class K>
class BSTree    //树
{
public:
	typedef BSTNode<K> Node;
    //成员函数...
private:
	Node* root = nullptr;
};

2.2 插入函数Insert

在树中插入一个节点,主要分为两类:

  1. 插入的节点是第一个节点(根节点)。
  2. 插入的节点不是根节点。

我们对于第一种情况,只需要做出简单的判断即可。对于第二种情况,则需要再分为两种情况:

  1. 插入的元素如果大于当前节点向右遍历
  2. 插入的元素如果小于当前节点向左遍历

直到遍历到叶子节点后,判断插入的元素与当前叶子节点的值的大小,比节点值大,插入在右边;比节点值小,插入在左边。

//在BSTree类中:
bool Insert(const K& data)
{
	Node* newnode = new Node(data);
	if (root == nullptr)
	{
		//插入的节点是根节点
		root = newnode;
	}
	else
	{
		//插入的节点不是根节点
		Node* cur = root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			if (data < cur->val)
			{
				//如果小于当前节点,向左遍历
				parent = cur;
				cur = cur->left;
			}
			else if (data > cur->val)
			{
				//如果大于当前节点,向右遍历
				parent = cur;
				cur = cur->right;
			}
			else
			{
				//与当前节点值相等,不做处理,当作插入失败
				cout << "Insert fail!" << endl;
				return false;
			}
		}
		//跳出循环时,cur的指向为空,实际上要在其父节点上插入新的节点,故需要记录父节点
		//重新比较父节点与新节点的值的大小关系
		if (data < parent->val)
		{
			parent->left = newnode;
		}
		else
		{
			parent->right = newnode;
		}
	}
    cout << "Insert success!" << endl;
	return true;
}

2.3 查找函数Find

 查找函数与插入函数中寻找插入节点的过程类似,只不过这次若是没有比当前节点的值大,也没有比当前节点的值小,就说明已经找到了。若是正常跳出循环,则说明没有找到。

//在BSTree类中:
bool Find(const K& data)
{
	if (root == nullptr)
	{
		cout << "Find fail!" << endl;
		return false;
	}
	else
	{
		Node* cur = root;
		while (cur != nullptr)
		{
			if (data < cur->val)
			{
				cur = cur->left;
			}
			else if (data > cur->val)
			{
				cur = cur->right;
			}
			else
			{
				//找到了
				cout << "Find success! The value is " << cur->val << endl;
				return true;
			}
		}
		//跳出循环说明没找到
		cout << "Find fail!" << endl;
		return false;
	}
}

2.4 删除函数Enrase

  删除函数比较复杂,主要分为四类:

  1. 节点的左右孩子都为空,直接删除该节点。
  2. 节点的左孩子为空,右孩子不为空,把该节点的右孩子赋值给该节点的父节点。删除该节点。
  3. 节点的左孩子不为空,右孩子为空,把该节点的左孩子赋值给该节点的父节点。删除该节点。
  4. 节点的左右孩子都不为空,这个时候不能直接删除节点,因为该节点的左右孩子没有位置移动。这种情况下,需要特殊处理。
  • 找到当前节点的,左子树最大值,或者右子树最小值的节点replace。
  • 把replace节点的值赋值给当前节点。
  • 再连接replace父节点与replace子节点。
  • 删除replace节点。

其中,1、2、3情况可以当作一种情况处理。 

图解: 

2.4.1 特殊情况处理

这种情况需要直接移动头节点,再删除原先的头节点。 
假设找的是右子树的最小值replace,这种时候需要记录replace的父节点replaceParent。如果初始化父节点为nullptr,这种情况就会出现空指针访问错误(由于需要判断replaceParent和replace的位置关系),所以父节点应该初始化为cur(寻找到的节点)而不是nullptr。
//在BSTree类中:
bool Enrase(const K& data)
{
	if (root == nullptr)
	{
		cout << "Enrase fail!" << endl;
		return false;
	}
	else
	{
		//第1、2、3种情况当作一种处理
		//先寻找到该节点
		Node* cur = root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			if (data < cur->val)
			{
				parent = cur;
				cur = cur->left;
			}
			else if (data > cur->val)
			{
				parent = cur;
				cur = cur->right;
			}
			else
			{
				//找到了,分情况讨论
				if (cur->left == nullptr)
				{
					//左边为空的情况
					if (parent == nullptr)
					{
						//说明没有更新节点,删除的是头节点
						root = cur->right; //移动根指针
						K v = cur->val;
						delete cur;
						cout << "Enrase success! The val is " << v << endl;
						return true;
					}
					//这里判断的是parent和cur的关系,cur在parent的哪边
					//链接cur孩子节点就链接在parent的哪边
					if (parent->left == cur)
					{
						parent->left = cur->right;
					}
					else if (parent->right == cur)
					{
						parent->right = cur->right;
					}
					K v = cur->val;
					delete cur;
					cout << "Enrase success! The val is " << v << endl;
					return true;
				}
				else if (cur->right == nullptr)
				{
					//右边为空的情况
					if (parent == nullptr)
					{
						//说明没有更新节点,删除的是头节点
						root = cur->left; //移动根指针
						K v = cur->val;
						delete cur;
						cout << "Enrase success! The val is " << v << endl;
						return true;
					}
					//这里判断的是parent和cur的关系,cur在parent的哪边
					//链接cur孩子节点就链接在parent的哪边
					if (parent->left == cur)
					{
						parent->left = cur->left;
					}
					else if (parent->right == cur)
					{
						parent->right = cur->left;
					}
					K v = cur->val;
					delete cur;
					cout << "Enrase success! The val is " << v << endl;
					return true;
				}
				else
				{
					//两边都不为空的情况
					//先找到当前节点左子树的最大值,或者右子树的最小值
					//左子树最大值,左子树一直往右走
					//右子树最小值,右子树一直往左走
					Node* replace = cur->right;
					Node* replaceParent = cur;
					while (replace->left != nullptr)
					{
						replaceParent = replace;
						replace = replace->left;
					}
					//这个时候,replace已经指向了右子树的最小值,进行赋值
					K v = cur->val;
					cur->val = replace->val;
					//接下来链接replace父节点和replace孩子节点
					//之所以分类讨论是因为有可能删除的是头节点,这种时候replaceParent
					//是没有刷新值的,为了避免空指针,replaceParent初始化值为cur的值
					//由于链接需要判断:replaceParent和replace的位置关系,决定replace
					//的孩子节点链接到replaceParent的哪个节点上(left / right)
					//需要分类讨论
					if (replaceParent->left == replace)
					{
						replaceParent->left = replace->right;
					}
					else if (replaceParent->right == replace)
					{
						replaceParent->right = replace->right;
					}
					delete replace;
					cout << "Enrase success! The value is " << v << endl;
					return true;
				}
			}
		}
		//走到这里说明没找到,无法删除
		cout << "Enrase fail!" << endl;
		return false;
	}
}

2.5 遍历二叉搜索树

  二叉搜索树由于左子树的值都小于根节点的值,右子树都大于根节点的值,所以需要用中序遍历来打印。

public:
void InOrder()  //由于root根节点私有,外面拿不到,所以多套一层,在类中就能拿到了
{
	InOrder(root);
	cout << endl;
}
private:
void InOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
	InOrder(root->left);
	cout << root->val << ' ';
	InOrder(root->right);
}

三、源码

#include<iostream>
using namespace std;

template<class K>
class BSTNode
{
public:
	K val;
	BSTNode<K>* left;
	BSTNode<K>* right;
	BSTNode(const K& val)
		:val(val)
		,left(nullptr)
		,right(nullptr){}
};

template<class K>
class BSTree
{
public:
	typedef BSTNode<K> Node;
	bool Insert(const K& data)
	{
		Node* newnode = new Node(data);
		if (root == nullptr)
		{
			//插入的节点是根节点
			root = newnode;
		}
		else
		{
			//插入的节点不是根节点
			Node* cur = root;
			Node* parent = nullptr;
			while (cur != nullptr)
			{
				if (data < cur->val)
				{
					//如果小于当前节点,向左遍历
					parent = cur;
					cur = cur->left;
				}
				else if (data > cur->val)
				{
					//如果大于当前节点,向右遍历
					parent = cur;
					cur = cur->right;
				}
				else
				{
					//与当前节点值相等,不做处理,当作插入失败
					cout << "Insert fail!" << endl;
					return false;
				}
			}
			//跳出循环时,cur的指向为空,实际上要在其父节点上插入新的节点,故需要记录父节点
			//重新比较父节点与新节点的值的大小关系
			if (data < parent->val)
			{
				parent->left = newnode;
			}
			else
			{
				parent->right = newnode;
			}
		}
		cout << "Insert success!" << endl;
		return true;
	}
	bool Find(const K& data)
	{
		if (root == nullptr)
		{
			cout << "Find fail!" << endl;
			return false;
		}
		else
		{
			Node* cur = root;
			while (cur != nullptr)
			{
				if (data < cur->val)
				{
					cur = cur->left;
				}
				else if (data > cur->val)
				{
					cur = cur->right;
				}
				else
				{
					//找到了
					cout << "Find success! The value is " << cur->val << endl;
					return true;
				}
			}
			//跳出循环说明没找到
			cout << "Find fail!" << endl;
			return false;
		}
	}
	bool Enrase(const K& data)
	{
		if (root == nullptr)
		{
			cout << "Enrase fail!" << endl;
			return false;
		}
		else
		{
			//第1、2、3种情况当作一种处理
			//先寻找到该节点
			Node* cur = root;
			Node* parent = nullptr;
			while (cur != nullptr)
			{
				if (data < cur->val)
				{
					parent = cur;
					cur = cur->left;
				}
				else if (data > cur->val)
				{
					parent = cur;
					cur = cur->right;
				}
				else
				{
					//找到了,分情况讨论
					if (cur->left == nullptr)
					{
						//左边为空的情况
						if (parent == nullptr)
						{
							//说明没有更新节点,删除的是头节点
							root = cur->right; //移动根指针
							K v = cur->val;
							delete cur;
							cout << "Enrase success! The val is " << v << endl;
							return true;
						}
						//这里判断的是parent和cur的关系,cur在parent的哪边
						//链接cur孩子节点就链接在parent的哪边
						if (parent->left == cur)
						{
							parent->left = cur->right;
						}
						else if (parent->right == cur)
						{
							parent->right = cur->right;
						}
						K v = cur->val;
						delete cur;
						cout << "Enrase success! The val is " << v << endl;
						return true;
					}
					else if (cur->right == nullptr)
					{
						//右边为空的情况
						if (parent == nullptr)
						{
							//说明没有更新节点,删除的是头节点
							root = cur->left; //移动根指针
							K v = cur->val;
							delete cur;
							cout << "Enrase success! The val is " << v << endl;
							return true;
						}
						//这里判断的是parent和cur的关系,cur在parent的哪边
						//链接cur孩子节点就链接在parent的哪边
						if (parent->left == cur)
						{
							parent->left = cur->left;
						}
						else if (parent->right == cur)
						{
							parent->right = cur->left;
						}
						K v = cur->val;
						delete cur;
						cout << "Enrase success! The val is " << v << endl;
						return true;
					}
					else
					{
						//两边都不为空的情况
						//先找到当前节点左子树的最大值,或者右子树的最小值
						//左子树最大值,左子树一直往右走
						//右子树最小值,右子树一直往左走
						Node* replace = cur->right;
						Node* replaceParent = cur;
						while (replace->left != nullptr)
						{
							replaceParent = replace;
							replace = replace->left;
						}
						//这个时候,replace已经指向了右子树的最小值,进行赋值
						K v = cur->val;
						cur->val = replace->val;
						//接下来链接replace父节点和replace孩子节点
						//之所以分类讨论是因为有可能删除的是头节点,这种时候replaceParent
						//是没有刷新值的,为了避免空指针,replaceParent初始化值为cur的值
						//由于链接需要判断:replaceParent和replace的位置关系,决定replace
						//的孩子节点链接到replaceParent的哪个节点上(left / right)
						//需要分类讨论
						if (replaceParent->left == replace)
						{
							replaceParent->left = replace->right;
						}
						else if (replaceParent->right == replace)
						{
							replaceParent->right = replace->right;
						}
						delete replace;
						cout << "Enrase success! The value is " << v << endl;
						return true;
					}
				}
			}
			//走到这里说明没找到,无法删除
			cout << "Enrase fail!" << endl;
			return false;
		}
	}
	void InOrder()
	{
		InOrder(root);
		cout << endl;
	}
private:
	void InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		InOrder(root->left);
		cout << root->val << ' ';
		InOrder(root->right);
	}
	Node* root = nullptr;
};
### 关于 PTA 平台上二叉搜索树的相关练习题及其解法 #### 了解二叉搜索树的基础概念 在深入探讨具体的练习之前,重要的是要理解什么是二叉搜索树(BST)。这是一种特殊的二叉树,在这种树中,对于任意节点而言,左子树中的所有键值小于该节点的键值;右子树中的所有键值大于该节点的键值[^1]。 #### 构建二叉搜索树 构建BST的过程涉及按照特定顺序插入新元素。每当有新的数值加入时,程序会从根部开始遍历整棵树直到找到合适的位置放置这个数。如果当前考察位置为空,则直接在此处创建一个新的叶子节点来容纳待插入的数据项。这一过程不仅有助于掌握基本的操作技能,而且可以增强对数据结构内部机制的认识。 #### 寻找共同祖先问题 当面对诸如“求两个指定节点之间的最低公共祖先”的挑战时,一种有效的方法是从根向下追踪路径直至遇到任一目标节点为止,并记录沿途访问过的每一个节点作为候选者。接着继续探索另一条分支并重复上述动作。最终对比两条路线所遇见过的所有节点集合,其中最深的那个交集成员即是答案所在之处[^3]。 ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def lowestCommonAncestor(root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': if not root or root == p or root == q: return root left_search_result = lowestCommonAncestor(root.left, p, q) right_search_result = lowestCommonAncestor(root.right, p, q) if left_search_result and right_search_result: return root elif left_search_result: return left_search_result else: return right_search_result ``` 此代码片段展示了如何实现寻找最小公共祖先的功能。它采用递归的方式分别检查左右两侧是否存在所需的目标节点p和q。一旦发现两者皆存在于不同方向之下,则说明当前处理的对象正是它们之间最近的一个共有前辈。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Celia~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值