数据结构——二叉搜索树深度解析

前言

知道我c++系列的同学,都知道,我喜欢将理论知识和实际代码穿插着讨论,所以再我们讲了继承和多态偏理论部分的内容后,我接下来还是回归stl的实现章节,但是接下来的map个set等结构都是依托树的结构,所以我这里也希望帮准你们,同时也是巩固自己这部分的知识,加油。

1.二叉搜索树的概念

听到二叉搜索树是不是感觉在骂人,哈哈,但是他的本名就是二叉搜索树,英文名叫BST(Binary Search Tree).

之前我在堆的章节中谈论过二叉树,这里搜索树,就是在,之前二叉树的基础上加上一个规则:
左子树的值 < 根的值<右子树的值,同时一个二叉搜索树中没有重复的数据。

1.1搜索树性质

再次强调搜索树的这个规则:
左子树的值 < 根的值<右子树的值

这个规则非常重要,我们从这个规则就知道,如果将一个二叉搜索树通过中序遍历给打印出来,那么打印出来后的数据,一定是按照升序排列的。同时要知道,根的右子树部分的任何一个数据都大于根,根的左子树的部分的任何一个部分都小于根的值。

2.二叉搜索树的操作

如图所示就是一个二叉搜索树。你看无论使这个节点是作为根还是作为孩子节点,他都满足自己的左子树的值小于根,右子树大于根的值的规则。

2.1二叉搜索树的插入

一个二叉树的树插入要符合他的规则,要注意的是,因为二叉搜索树里面并没有重复元素,所以每当我们插入一个节点,它一定是在一个叶子节点的后面插入的,至于是左孩子还是有孩子,就要自己判断了。

2.2二叉搜索树的查找

a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。

b、最多查找高度次,走到到空,还没找到,这个值不存在。

2.3二叉搜索树的删除

在二叉树的所有操作中,最难的操作就是,二叉树的删除,因为找到我们要删除的节点后,有下面四种情况:

a. 要删除的结点无孩子结点

b. 要删除的结点只有左孩子结点

c. 要删除的结点只有右孩子结点

d. 要删除的结点有左、右孩子结点

看似有四种情况,其实a和b或c可以综合成一种情况,因此真正的删除过程是:

情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除

情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除

情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除

3.二叉搜索树的代码实现

3.1二叉搜索树的框架

写出一个二叉搜索树,首现我们要写出他的节点,因为每个节点,既有自己的值,又有指向子节点的指针,所以我们要把它封装成一个类:

一次我们的二叉搜索树的类只需要,存放一个节点,然后把这个节点,作为根节点即可:

Node是我typedef Treenode后的名称,值得注意的是,因为我们的成员是一个自定义类型的指针,自定义类型的初始化,编译器会调用他自己的构造函数,但是我们的这个指针必须自己在构造函树里面初始化,因为后面的任何操作,都是在root后面进行的,因此必须对root指针进行初始化:

3.2搜索二叉树插入的代码实现

搜索二叉树的插入,只用记住两点即可,1是我们插入时要满足搜索二叉树的父子大小关系,而是我们一定是在一个叶子节点后插入的。同时,因为我们最后要和叶子节点比较,然后插入,所以必须保留自己的父节点:

同时看前几行代码意思是判断,你插入的搜索树是不是一个空树,如果时空树,那么直接把新节点赋值给根节点即可。

3.3二叉搜索树查找代码实现

同样这里的我们一样按照循环的方式遍历这个二叉树,值大于根就往右遍历,比根节点的值小,就向左子树遍历。

3.4二叉搜索树删除节点操作的实现

这是最难同时也是最重要的一部分,我们现找到这个节点:

注意,如果我们走到了else,那么就证明我们找到了要删除的节点。

接下来就是要删除节点的情况分析。

3.4.1被删除节点只有右孩子

如上图就是一个二叉搜索树,如果我们要删除的节点就是根节点8呢?

所以这里同样再次分两种请况,一种是根节点,一种不是根节点。

如果是根节点:

那我们可以直接将10的节点赋值给根节点即可:

如过我们要删除的节点是10呢?

对于10这中节点情况我们可以归为,被删除节点是自己父节点的右孩子,还有就是11,这中情况,被删除节点是父节点的左孩子,因此又是两种情况:

3.4.2被删除的节点只有右孩子

同上面的情况,我们只有有孩子的分析同样分为:

被删除节点是否为根节点的讨论:

以及被删除节点是自己父节点的左右孩子的讨论:

所以上面两部分的讨论就出来了。

3.3.3被删除节点既有右孩子,又有左孩子

如上图如果我们要删除节点8,你会怎么做?

既然8被删除了,我们就一定要,再选出一个节点去顶替8的位置,而且在其位,还有谋其职,顶替后的节点我们还要满足左<根<右的规则。那么应该选谁呢?

此时我们就要记得我们的搜索树的性质:

根的右部分肯定比根大,根的左部分,肯定比根小。所以我们不妨选:
被删除节点左子树的最大值或被删除节点右子树的最小值。

左子树的最大值,不就是左子树的右孩子遍历吗?

右子树的最小值,不就是有紫苏的左孩子的遍历吗?

对于上图来说,我们即可以选7,也可以选10?

这里我们按照左子树的最大值来算:

这样我们就找到了,左子树的最大值然后我们要做的就是先交换被删除节点的值然后,该节点是否有左孩子,如果还有左孩子,就要同他的父节点链接在一起。

,看到这里,你似乎感觉一切都结束了,但是,如果我们要删除的节点是6呢?你还能这样写吗,你如果还是按照上面的逻辑来写,那么你的parent根本就不会进入循环。因为,leftmax的下一个右孩子就是空 ,因此针对这种情况我们就是要判断,遍历后的leftmax是不是还是等于被删除节点的左孩子,如果还是,我们就要另作处理:

3.5删除操作的总代码

bool erase(const K val)

{

Node* cur = _root;

Node* parent = nullptr;

while (cur)

{

if (val > cur->_val)

{

parent = cur;

cur = cur->right;

}

else if (val < cur->_val)

{

parent = cur;

cur = cur->left;

}

//这里找到了要删除的节点

else

{

if (cur->left == nullptr&&cur->right!=nullptr)

{

if (cur == _root)

{

_root = cur->right;

}

else {

if (parent->right == cur)

{

parent->right = cur->right;

}

else

{

parent->left = cur->right;

}

delete cur;

}

}

else if (cur->right == nullptr&&cur->left!=nullptr)

{

if (cur == _root)

{

_root = cur->left;

}

else

{

if (parent->left == cur)

{

parent->left = cur->left;

}

else

{

parent->right = cur->left;

}

delete cur;

}

}

//处理cur的左右都不是空的情况

else

{

Node* leftmax = cur->left;

复制代码
			while (leftmax->right)
			{
				parent = leftmax;
				leftmax = leftmax->right;
			}
			if (leftmax != cur->left)
			{
				cur->_val = leftmax->_val;
				if (leftmax->left)
				{
					parent->right = leftmax->left;

				}
			}
			if (leftmax == cur->left)
			{
				cur->left = leftmax->left;
			}
			cur = leftmax;
			delete cur;
		}
		return true;
	}

}
return false;

}

4.搜索二叉树的递归实现

既然我们在讨论二叉树,那么肯定是要用到递归的,针对于递归实现的操作,我们这里只是心啊insert和erase即可。

4.1递归实现插入

针对递归实现的操作,我认为根循环的方式比,更加简单,因为,二叉搜索树的插入,一定是在叶子节点的后面插入的因此,我们只用通过递归找到被插入的叶子节点即可:

好现在我们找到了空指针,那么怎么插入呢,新节点要怎么和父节点比较呢?其实这里我们不妨这样做:

注意看我的参数是Node*&的类型,是node*的引用,那么此时的root就是我们要找的位置,我们直接插入即可,完整代码如下:

4.2递归实现删除

递归实现的删除就同样要求我们先找到,被删除的节点:

注意到else这部分后我们找到了,被删除节点,然后就是和上面循环实现的删除,一样分为三种情况

只有左孩子:

只有右孩子:

同时右左右孩子:

先找到左子树的最大节点:

找到后,和被删除节点的值进行交换,接下来的删除节点,和链接子节点的过程,我们可以通过递归来实现,即不断将被删除节点,放到最后一个位置中去,知道为空:

bool _Eraser(Node*& root, const K val)

{

Node* del = root;

if (root == nullptr)

{

return false;

}

复制代码
if (val > root->_val)
{
	return _Eraser(root->right, val);
}
else if (val < root->_val)
{
	return _Eraser(root->left, val);
}
else
{
	if (root->right == nullptr && root->left)
	{
		root = root->left;
	}
	else if (root->right == nullptr && root->left != nullptr)
	{
		root = root->left;
	}
	else
	{
		if (root->left == nullptr) {
			root = root->right;
		}
		else if (root->right == nullptr) {
			root = root->left;
		}
		else
		{
			Node* leftmax = root;
			while (leftmax->right)
			{
				leftmax = leftmax->right;
			}
			std::swap(leftmax->_val, root->_val);
			return _Eraser(root->left, leftmax->_val);
		}
	}
	delete del;
	return true;
}

}

5.总结

关于二叉搜索树的知识,我认为应该自己实现一遍删除的操作,自己真是的体会总结,删除的不同情况,并进行总结,这样才能更好的掌握,这些二叉搜索树的知识。

相关推荐
大数据张老师4 小时前
数据结构——希尔排序
数据结构·算法·排序算法·1024程序员节
扫地的小何尚4 小时前
一小时内使用NVIDIA Nemotron创建你自己的Bash计算机使用智能体
开发语言·人工智能·chrome·bash·gpu·nvidia
MoonBit月兔5 小时前
MoonBit Pearls Vol.12:初探 MoonBit 中的 JavaScript 交互
开发语言·javascript·数据库·交互·moonbit
第七序章5 小时前
【C + +】unordered_set 和 unordered_map 的用法、区别、性能全解析
数据结构·c++·人工智能·算法·哈希算法·1024程序员节
草莓熊Lotso5 小时前
《算法闯关指南:优选算法--二分查找》--23.寻找旋转排序数组中的最小值,24.点名
开发语言·c++·算法·1024程序员节
foundbug9995 小时前
C# 实现 Modbus TCP 通信
开发语言·tcp/ip·c#
文火冰糖的硅基工坊5 小时前
[嵌入式系统-150]:智能机器人(具身智能)内部的嵌入式系统以及各自的功能、硬件架构、操作系统、软件架构
android·linux·算法·ubuntu·机器人·硬件架构
郝学胜-神的一滴5 小时前
主成分分析(PCA)在计算机图形学中的深入解析与应用
开发语言·人工智能·算法·机器学习·1024程序员节
JuicyActiveGilbert5 小时前
【Python进阶】第2篇:单元测试
开发语言·windows·python·单元测试