BSTree
- 0 引言
- 1 二叉搜索树的概念
- 2 创建一棵二叉搜索树(插入操作)
- 2.1 画图分析插入操作
- 2.2 代码思路
- 2.3 利用中序遍历验证
 
- 3 二叉搜索树的查找操作
- 4 二叉树搜索树的删除操作(重点)
- 4.1 代码的一些细节分析
 
- 5 总结
0 引言
本篇文章会在VS2019下以代码+图片的方式一步一步分析二叉搜索树的插入、删除、查找操作。文章的重点会放在删除操作上。
1 二叉搜索树的概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
如图所示是一棵二叉搜索树:
 
 值得一提的是,因为二叉搜索树独特的性质,我们对它进行中序遍历后,就会发现得到的结果是已经排序好的。所以也叫二叉排序树
如上图,中序遍历后的结果是:
0 1 2 3 4 5 6 7 8 9
2 创建一棵二叉搜索树(插入操作)
首先,我们要定义出树的结点的结构体
template <class K>
struct BSTreeNode //定义结点的结构体
{
	BSTreeNode<K>* left;
	BSTreeNode<K>* right;
	K _key;
	BSTreeNode(const K& key)//初始化
		:left(nullptr), right(nullptr), _key(key)
	{}
};
然后开始设计二叉搜索树的结构
template <class K>
class BSTree
{
	typedef BSTreeNode<K> Node;//节点名称
public:
  //插入,删除,查找操作函数写在这
private:
	Node* root = nullptr;//初始化根结点为空
};
接下来就是利用插入操作来创建出一棵二叉搜索树
 我们初始数据就以这幅图为例
 
int a[] = { 5,3,7,1,4,6,8,0,2,9 };
2.1 画图分析插入操作
插入的规则就是,要插入的结点从根结点开始比较,比当前结点大就往右继续比较,比当前结点小就往左比较,直到比较到空为止。如果当前结点等于我要插入的值,就不能再继续比较了,因为二叉搜索树不允许有重复的元素。
 
 首先,插入的时候,原先数组的数据的顺序是会影响到二叉搜索树的结构的。因为我们是按照数组的从左往右遍历插入的。
 这里一开始root为空,会先创建一个5结点。
 然后按照数组顺序,创建一个3结点,由于3比5小,所以3结点会在5结点的左子树
 然后继续准备插入7结点,由于7比5大,所以7结点会在5结点的右子树。
 以此类推,完整的二叉搜索树如下

2.2 代码思路
插入操作的代码思路很简单,几个判断语句就搞定了。
 需要注意的是,我们比较到空的时候,需要记录上一个结点才能插入,否则无法和上一个结点链接上。
bool insert(const K& key)//插入成功返回true,插入失败返回false,遇到重复元素就会返回false
	{
		if (root == nullptr)//如果一开始为空,该树就没有结点,创建一个新结点就即可
		{
			root = new Node(key);
			return true;
		}
		else//一开始不为空,需要插入的元素从根结点开始比较了
		{
			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//如果相等的话就返回false,因为不允许有重复元素
				{
					return false;
				}
			}
			//代码走到这里说明cur为空了,我们利用这个cur创建一个结点
			cur = new Node(key);
          //比较要插入的元素和上一个结点的大小
          //比上一个结点大就插入右边
			if (key > parent->_key)
			{
				parent->right = cur;
			}
			else//否则插入左边
			{
				parent->left = cur;
			}
		}
		return true;//代码走到这里说明插入成功
	}
2.3 利用中序遍历验证
这是写在类里面的中序遍历代码
 这里有个小技巧,我们嵌套了函数,方便调用的时候可以不用传参
	void _InOrder(Node* root)//中序遍历
	{
		if (root == nullptr) return;
		_InOrder(root->left);
		cout << root->_key << " ";
		_InOrder(root->right);
	}
	void InOrder()//要调用的函数
	{
		_InOrder(root);
	}
代码执行结果如下:
 
3 二叉搜索树的查找操作
查找操作和刚才的插入操作思路差不多,都是从根结点开始比较,不过查找操作是找到该元素为止或者走到空为止。
比如,我们现在要插入2这个元素,2比5小往左子树走,2比3小,往左子树走,2比1大,往右子树走,这时候找到了2这个元素,停止比较。
 
直接上代码:
bool find(const K& key)
	{
		if (root == nullptr) return false;//如果根结点为空,就返回false,表示未找到
		Node* cur = root;
		while (cur)//从根结点比较
		{
			if (cur->_key < key)//要查找的元素比当前元素大,就往右子树走
			{
				cur = cur->right;
			}
			else if (cur->_key > key)//否则就往左子树走
			{
				cur = cur->left;
			}
			else//相等,返回true,说明找到了
			{
				return true;
			}
		}
		return false;//走到这里说明cur为空了还未找到,二叉搜索树里没有该结点,直接返回false
	}
测试代码如下:
 
 
4 二叉树搜索树的删除操作(重点)
二叉搜索树的重头戏是删除操作,因为删除了一个结点,可能会影响到多个结点,也可能不会影响。接下来,我们分类讨论。
1.被删除的结点是叶节点
这种情况比较简单,因为在叶节点上,删除后不会影响到其他结点,我们直接删除即可。
如图,我们要删除9这个叶结点,直接删除即可。
 
2.被删除的结点只有一个孩子
如果被删除的结点只有一个孩子,我们直接让被删除的结点的父结点领养这个孩子即可。
 之所以能被领养,是因为一个结点能管理两个孩子,所以这种思路是可行的。
 如图,我们要删除8这个结点,肯定是不能直接删除,因为它还有一个孩子,此时让8的父节点领养这个孩子即可。
 
3.被删除的结点有左右两个孩子
被删除的结点有两个孩子的话,就不能像情况2那样找父节点领养了。
 那么我们可以想想二叉搜索树的性质:左结点比当前结点小,右节点比当前结点大,且子树也满足此规则。那么也就是说,左子树的所有元素都比当前结点小,右子树的所有元素都比当前结点大。
那么,如果我此时要删除3这个结点,是不是可以用3的左子树里最大的结点或者3的右子树里最小的结点替代掉3。
 得益于二叉搜索树这种特殊的性质,很容易验证得到:是可以的。
 
3结点左子树的最大值替换3

3结点右子树的最小值替换3

4.1 代码的一些细节分析
我们先假设已经找到了要删除的结点,为cur
先来看情况1和情况2,如果是叶结点直接删除和如果要删除的结点只有一个孩子,就找父节点领养该孩子。
 那么可以统一处理,把叶节点链接的nullptr也看作孩子。那么就统一成左为空和右为空的情况了。
这里要处理这两种情况
1.如果要删除的是根结点的话,这个根结点在这里肯定只有左子树或者右子树,我们直接拿根节点的下一个非空结点当根结点即可。
2.如果不是非根结点,直接按照上面的规则即可,记录要删除结点的父节点,然后领养该结点的孩子
2.1 左为空的情况,如果要删除9结点的话,那么让parent->right = cur->right即可
 
 或者是删除8结点的话,也是让parent->right = cur->right
 
 2.2 右为空的情况,我们要删除7这个结点,让parent->right = cur->left

 注意:需要判断cur是parent的左节点还是右节点
			    if (cur->left == nullptr)//如果cur右不为空
				{
					if (cur == root)//如果要删除的结点是根
					{
						root = cur->right;//直接用根的右结点替换根
					}
					else//如果要删除的结点不是根
					{
						if (parent->left == cur)//记录的父节点的左节点是当前要删除的结点
						{
							parent->left = cur->right;//直接让父节点领养
						}
						else//记录的父节点的右结点是当前要删除的结点
						{
							parent->right = cur->right;//直接让父节点领养
						}
					}
					delete cur;//删除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;
				}
我们最后来看一下情况3,要删除的结点有左右两子树
我们这里采用右子树的最小结点要替换要删除的结点。那么我们需要找到该结点右子树的最小结点。
Node* ppminRight = cur;//cur为要删除的结点,记录最小结点的父节点
Node* minRight = cur->right;//找cur右子树最小的结点
while (minRight->left)//如果minRight的左节点为空就停下
{
	ppminRight = minRight;
	minRight = minRight->left;
}
我们来看以下这两种情况
 1.要删除7结点,此时minRight是存在的,minRight要替换掉cur,但是minRight还有自己的右子树,此时我们让父结点领养即可,ppminRight->left = minRight->right
 

 2.要删除5结点,此时minRight为7,是5这个结点的右子树的最小值,也是ppminRight的右节点
 此时让ppminRight->right = minRight->right

cur->_key = minRight->_key;//找到了以后就直接交换值即可
if (ppminRight->left == minRight)//判断minRight是否ppminRight的左子树
{
	ppminRight->left = minRight->right;//ppminRight领养minRight的右子树
}
else//否则就就让ppminRight领养minRight的右子树
{
	ppminRight->right = minRight->right;
}
delete minRight;
完整代码:
	bool erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = root;
		while (cur)
		{
			if (cur->_key < key)
			{
				cur = cur->right;
			}
			else if (cur->_key > key)
			{
				cur = cur->left;
			}
			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* ppminRight = cur;
					Node* minRight = cur->right;
					while (minRight->left)
					{
						ppminRight = minRight;
						minRight = minRight->left;
					}
					cur->_key = minRight->_key;
					if (ppminRight->left == minRight)
					{
						ppminRight->left = minRight->right;
					}
					else
					{
						ppminRight->right = minRight->right;
					}
					delete minRight;
				}
				return true;
			}
		}
		return false;
	}
5 总结
以上就是二叉树搜索树的插入、查找、删除操作。完整的代码如下,读者可自行测试。
 如有错误,欢迎指出。
 https://gitee.com/F_F_G/structure/blob/master/BinaryTree/BinaryTree/BTree.h





















