【二叉树进阶】搜索二叉树(递归+非递归两种版本详解)

news2024/9/24 1:18:27

文章目录

  • 前言
  • 1. 二叉搜索树的概念
  • 2. 二叉搜索树的结构
    • 2.1 结点结构
    • 2.2 树结构
  • 3. 插入操作(非递归)
    • 3.1 思路分析
    • 3.2 代码实现
    • 3.3 中序遍历(测试用)
  • 4. 查找操作(非递归)
    • 4.1 思路分析
    • 4.2 代码实现
  • 5. 删除操作(非递归)-重难点
    • 5.1 情况分类及思路分析
    • 5.2 代码实现
  • 6. 查找(递归版本)
    • 6.2 思路分析
    • 6.2 代码实现
  • 7. 插入(递归版本)
    • 7.1 思路分析
    • 7.2 代码实现
  • 8. 删除(递归版本)
    • 8.1 思路分析
    • 8.2 代码实现
  • 9. 其它相关成员函数的实现
    • 9.1 析构
    • 9.2 构造和拷贝构造
    • 9.3 赋值重载
  • 10. 完整代码
    • 10.1 BSTree.h
    • 10.2 BSTree.cpp

前言

二叉树在前面C数据结构阶段已经讲过,本节取名二叉树进阶是因为:

  1. map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构。
  2. 二叉搜索树的特性了解,有助于更好的理解map和set的特性。
  3. 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘。
  4. 有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。

因此本节用C++语言对二叉树部分进行收尾总结。

我们之前学的普通的二叉树其实意义不大,因为如果只是用二叉树来存储数据的话,还不如直接用链表或者顺序表等这些顺序结构。

那二叉树搜索树相对来说,就比较有意义了。

1. 二叉搜索树的概念

那什么是二叉搜索树呢,先来了解一下它的概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
在这里插入图片描述

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树(即它的每一棵子树也满足其左子树所有结点的值都小于根结点的值,右子树的所有结点的值大于根结点的值)

在这里插入图片描述

为什么又叫二叉排序树呢?

仔细观察我们会发现如果对一棵搜索二叉树进行中序遍历的话
在这里插入图片描述
其实就能得到一个结点值的升序序列。

那了解了搜索二叉树的概念,接下来我们就来手撕一个搜索二叉树。

2. 二叉搜索树的结构

首先我们来定义一下结点和搜索二叉树的结构

我们之前定义一个模板类,一般都喜欢用T(type),但是在这里比较喜欢用K(key)

2.1 结点结构

在这里插入图片描述

2.2 树结构

在这里插入图片描述

相信大家很容易都能看懂,那这里我就不详细解释了。

3. 插入操作(非递归)

接下来我们来实现一下向搜索二叉树中插入元素的操作。

3.1 思路分析

首先对于搜索二叉树来说,它的插入应该有插入成功和插入失败(因为搜索二叉树一般不允许出现重复元素)两种情况。

我们来分析一下

首先看插入成功的情况:

在搜索二叉树中,要插入一个元素时,如果可以 插入,那么它插入的位置一定是确定的。
举个栗子
在这里插入图片描述
还是以这棵树为例,假设我们现在要插入一个12
那要怎么做呢?
其实就是从根结点开始,去找出那个合适的位置,然后把12放进去。
根结点的值是8,12大于8,所以应该去右子树找,8的右子树是10,12依然大于10,那再看10的右子树,是14,此时12小于14,所以就要往14的左子树,14的左子树为13,12小于13,所以再看13的左子树,是空。
所以,12就应该放在13的左子树上。

在这里插入图片描述
此时就插入成功了

那失败的情况呢?

比如,我们现在要插入一个13
在这里插入图片描述
首先还是根据大小去比较,找合适的位置,但是走到13的位置发现要插入的值和已经存在的值相等,那就直接return false,插入失败。

当然,如果插入的是第一个结点,那就不需要比较了,直接让它成为根结点。

3.2 代码实现

那我们来写一下代码
在这里插入图片描述

首先第一个插入的结点是比较特殊的,因为第一个要作为根结点:

那怎么判断是不是第一个插入的呢?
🆗,插入第一个的时候,根结点是不是还是空的啊
所以
在这里插入图片描述
如果根结点为空,那就证明是第一次插入,就把它作为根结点。

那其它情况呢?

那后续的插入,就需要我们从根结点开始比较大小去找到合适的位置插入了,思路上面已经讲过了,这里就直接写代码了
在这里插入图片描述
按照我们上面讲的思路,走到红框这里,就走到key要插入的正确位置了。

那现在问题来了,如何正确的插入key对应的结点并链接到搜索二叉树上?

大家看这样可以吗
在这里插入图片描述
有没有什么问题?

🆗,这样写会存在两个问题:
第一个

这里的cur是一个函数里面的局部变量,函数调用结束,cur这个指针变量就被销毁了,销毁了不说,目前我们这样写是不是还会存在内存泄漏啊,cur被销毁了,但是它指向的空间还没释放。
那cur指向的空间不是属于这棵二叉树了嘛,不是最终可以随着搜索树的析构释放吗?
🆗,你把key的结点赋给cur,就链接到树上了嘛,并没有

这就是第二个问题:

把结点给cur,并没有链接到树上。
还看这个例子
在这里插入图片描述
走到上面红框的地方,cur只是存了13这个结点(假设取名为parent)的左孩子指针(即parent->left)的一个指针变量,相当于是parent->left的拷贝,把结点赋给它,并没有真正链接到13的左孩子上。

那怎么解决呢?

🆗,我们在cur不断向左或向右去找到过程中,记录它的父亲结点,最终cur走到正确的位置,把key的结点直接链接到父亲结点上就可以了
我们来修改一下
在这里插入图片描述

代码:

	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->right;
			}
			else
			{
				return false;
			}
		}
		//走到这里cur为空,就是key应该插入的位置
		cur = new Node(key);
		//链接
		if (key < parent->_key)
			parent->left = cur;
		if (key > parent->_key)
			parent->right = cur;
		return true;
	}

实现好了,行不行呢?测试一下:

构建我们示例中的那棵树,看看结果一不一样:
在这里插入图片描述
在这里插入图片描述
那我们构建好可以中序遍历打印一下看看,中序遍历刚好是一个升序。

3.3 中序遍历(测试用)

那我们写个中序遍历

二叉树的遍历我们在初阶已经学过,相信现在对于大家已经很简单了
在这里插入图片描述
ps:之前left和right没加_,现在补上了

那写好我就可以调了,但会有一个问题:

在这里插入图片描述
这里调用得传根结点(因为要递归,这个参数不能省),但是类外无法访问私有成员_root,把它放成共有的不太好。

那大家思考一下这里可不可以给缺省值?

缺省值给一个_root不就行了。
🆗是不行的,因为缺省值必须是常量或者全局变量(但一般不用全局变量)
这个我们在C++的第一篇文章有提到,大家可以去复习。
而且在参数列表其实根本拿不到成员变量_root,因为访问非静态成员要用this指针,而this指针只能在成员函数内部使用,参数列表也不行。

两个解决方法:

提供一个GetRoot的成员函数/方法,传参的时候通过该方法获取_root。
但C++里不喜欢这样
那其实有另外一种比较简便得方法,给InOrder函数在套一层(封装一下)
在这里插入图片描述
这样调的时候就不用传参了,当然我就可以把_InOrder变成私有的了

然后我们来测试一下

在这里插入图片描述
就可以了。

4. 查找操作(非递归)

接着我们来看一下查找:

4.1 思路分析

那查找其实是比较简单的:
我们要查找某个结点,那就从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找,找到的话返回true
在这里插入图片描述
最多查找高度次,走到空,还没找到,则这个值不存在,返回false
在这里插入图片描述

4.2 代码实现

写一下代码:

	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}
		return false;
	}

测试一下:

在这里插入图片描述
没问题

5. 删除操作(非递归)-重难点

那如果要删除二叉搜索树中的某个结点,应该怎么处理呢?

5.1 情况分类及思路分析

首先查找元素是否在二叉搜索树中,如果不存在,则直接返回false, 否则要删除的结点可能分下面四种情况:

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

比如:
在这里插入图片描述
我们现在要删除4这个结点
那这种情况是不是直接删除就好了,把4这个结点释放,让6的左孩子指向空就行了。

  1. 要删除的结点只有右孩子结点,左为空

比如我们现在要删除6
在这里插入图片描述
6要怎么删,那这里关键在于把6删了他还有一个孩子怎么处理?
🆗,现在这种情况我们要删除的结点6只有一个孩子,而6被删除的话它的父亲结点3就只剩下一个孩子了,而二叉树中一个结点最多可以有两个孩子,所以6被删除之后,我们是不是可以把它的孩子7托管给6的父亲3的右孩子结点啊。
在这里插入图片描述
不一定都是托给右,要进行判断,要看被删除结点是父结点的左还是右孩子
在这里插入图片描述

  1. 要删除的结点只有左孩子结点,右为空

在这里插入图片描述
比如我们现在要删除14
那这种情况处理方法其实和上一个一样,也是把被删除结点的孩子托管给其父亲结点,那这里也是托给父亲的右孩子。
另外其实第一种情况也可以归到第二种或第三种里面,第一种是两个孩子都为空,那也可以归到左为空或者右为空的情况里面。

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

在这里插入图片描述
比如我们要删除3或8,怎么删?
首先这里我们上面用到的托管给父结点的方法就不管用了,因为每个结点最多管两个孩子,而现在要删除的这个结点就有两个孩子,如果父亲原本就有两个,那这样父亲要管三个就超了。
况且对于8也就是根结点来说,它根本没有父结点。

那这种情况该如何处理呢?

对于这种情况我们使用的方法叫做——替换法删除法/伪删除法

在这里插入图片描述
以删除8为例,大家看,如果把8删了,谁能够坐到8这个位置呢?
那对于8这棵树来说,其实这个替代者可以有两个人选:

  1. 左子树的最大值
  2. 右子树的最小值

在这里插入图片描述
那对于当前这棵树其实就是7或者10。
其实仔细观察我们会发现,对于一棵搜索二叉树来说
整棵树中最大的结点就是最右边的那个结点,最小的结点就是最左边的那个结点。
那对于子树来说也是这样,我们看到7其实就是左子树的最右边的结点,10就是右子树最左边的结点。
所以这里,要想8,可以选择用7替换也可以用10。

以用7为例,怎么进行替换删除呢?

在这里插入图片描述
把7这个结点的值赋给8这个结点,然后把原始的7结点删除了就行了

那总结一下:

虽然上面我们分析了四种情况,但是我们说了第一种即被删除结点没有孩子的情况可以归到左为空或者右为空的情况里面。
所以总共有三种:

  1. 左为空
  2. 右为空
    这两种都是托管,但注意具体的代码处理是不一样的,因为一个右为空,一个左为空。
  3. 左右都不为空
    替换删除/伪删除法

5.2 代码实现

那下面写一下代码吧

首先我们得查找要删除的元素:

在这里插入图片描述

那接下来就是实现删除的逻辑了

左为空或者右为空得删除其实比较好处理,托管给父亲结点即可

在这里插入图片描述

最后来实现一下比较难搞的替换删除:

那我们上面说了这个替换结点可以是左数得最大结点(最右边的那个结点),也可以是右树得最小结点(最左边)。
那这里我选择右树的最小结点。
这里的替换删除分为两步:
第一步——找到右树最小结点,然后替换要删除的结点
在这里插入图片描述
第二步——删除替换结点
那这里其实有一些需要注意的地方:
我们在这里选择的是右树的最小结点,即右子树最左边的结点,那既然是最左边,他一定没有左孩子了,但是,它可能会有右孩子。
比如像这样
在这里插入图片描述
所以这里删除替换结点也要用托管的方式去删,那就需要保存一下替换结点的父亲
那这里还有一些需要特别注意的点:
首先我问大家,现在不是需要保存替换结点的父结点吗?这个父结点的初始值可以给nullptr吗?
在这里插入图片描述
如果看上面那个例子是可以的,因为会进入循环更新parent的值。
但是如果是这样的情况呢?
在这里插入图片描述
大家看这种情况是不是不会进入while循环啊,所以pminRight不会更新,那后面托管的时候就会对空指针解引用,所以这里初始值可以赋cur,即要进行删除的那个结点(伪删除)。
Node* pminRight = cur;
另外我们上面的那个例子在这里插入图片描述
就这个,最终托管的时候是链接到父亲的左子树上,但是不要认为这里找到是最左结点就一定链接到左子树上。
我们现在这个例子:
在这里插入图片描述
是不是就是链接到pminRight的右子树上了,所以要去看minRight(即替换结点)在pminRight的那边。
所以补充完毕应该是这样的
在这里插入图片描述
最终删除成功,就return true。

但是呢,其实改到现在还有一个问题:

在这里插入图片描述
我们能看出来这个测试结果是不正确的。

问题出在哪里呢?

在这里插入图片描述
我们直接删除根是没什么问题的现在
在这里插入图片描述
但是,如果这样
在这里插入图片描述
大家看这种其实就是挂了,我们见过很多次了,调试一下
在这里插入图片描述
出现了一个空指针异常(这里是在删除8的时候跳出来这个异常的),怎么回事呢?

我们来分析一下:

在这里插入图片描述
这次我们先删除了10、14、13,所以在删除8的时候是这样的
在这里插入图片描述
那为什么此时再去删除8就出现parent是空指针的情况呢?
🆗,那问题在于
现在要删8,是右为空的情况
在这里插入图片描述
因为根结点没有父亲结点。
那我们怎么解决呢?
那对于这种情况我们可以单独处理一下,其实可以直接更改一下根结点,直接让3成为新的根,然后把8删了就行
在这里插入图片描述
在这里插入图片描述

那然后我们再来测试

在这里插入图片描述
就可以了,随便删
在这里插入图片描述

bool Erase(const K& key)
{
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			//key == cur->_key就是找到了,进行删除
			//1.左为空
			if (cur->_left == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_right;
				}
				else
				{
					if (cur == parent->_left)
						parent->_left = cur->_right;
					else
						parent->_right = cur->_right;
				}
				delete cur;
			}
			//2.右为空
			else if (cur->_right == nullptr)
			{
				if (cur == _root)
				{
					_root = cur->_left;
				}
				else
				{
					if (cur == parent->_left)
						parent->_left = cur->_left;
					else
						parent->_right = cur->_left;
				}
				delete cur;
			}
			else//左右都不为空
			{
				//这里选择右树的最小结点(最左边)替换
				Node* pminRight = cur;
				Node* minRight = cur->_left;
				while (minRight->_left)
				{
					pminRight = minRight;
					minRight = minRight->_left;
				}
				cur->_key = minRight->_key;
				//删除替换结点
				if (pminRight->_left = minRight)
				{
					pminRight->_left = minRight->_right;
				}
				else
				{
					pminRight->_right = minRight->_right;
				}
				delete minRight;
			}
			return true;
		}
	}
	//找不到直接返回false
	return false;
}

那搜索二叉树主要的操作其实就这些,但是呢,我们上面都是用循环实现的,那搜索二叉树这里呢其实也可以用递归去搞,这三个操作的递归实现我们也有必要去学一下。

6. 查找(递归版本)

查找用递归要怎么写呢?

在这里插入图片描述
在类里面定义的递归一般我们都要套一层写成这种,原因就和我们上面写中序遍历那里一样。

6.2 思路分析

那具体怎么实现呢?
我们来分析一下:

首先来回顾一下递归的思想:
它通常把一个大型复杂问题层层转化为一个与原问题相似的规模较小的问题来求解,每个子问题都可以进一步分解为更小的子问题,直到达到基本情况(终止条件),然后逐层返回结果,最终得到整个问题的解。
所以这里的思路是这样的:
查找嘛,那就从根结点开始,如果大于根结点的值,就转换为去右子树里面查找,如果小于根结点,就转换为去左子树查找,如果等于就直接返回;那对于子树也是这样,一步步转换为子问题。
如果最后都没找到,一直到空,那就返回false。

那我们写一下代码:

6.2 代码实现

这个没什么难度,我就直接上代码了:

bool FindR(const K& key)
{
	return _FindR(_root, key);
}

bool _FindR(Node* root, const K& key)
{
	if (root == nullptr)
		return false;
	if (key < root->_key)
	{
		return _FindR(root->_left, key);
	}
	else if (key > root->_key)
	{
		return _FindR(root->_right, key);
	}
	else
	{
		retrun true;
	}
}

试一下:

在这里插入图片描述

7. 插入(递归版本)

7.1 思路分析

在这里插入图片描述

那插入用递归怎么做呢?

那也是类似的思路,从根节点开始,如果要插入的结点大于根,就转换为去右子树插入;如果要插入的结点小于根,就转换为去左子树插入;如果相等,返回flase;如果一直走到空,那就就在该位置插入就行了。

7.2 代码实现

在这里插入图片描述

但是有一个问题,我们找到空插入的时候,如何和它的父亲链接起来?

我们可能会想到这样的方法,比如把父亲作为递归的参数进行传递,或者去判断root的子树为空而不是它本身为空。
但是,最好的方法我觉得是这样:
在这里插入图片描述
直接用root的引用就可以了。
因为引用的话,走到空,他就是那个位置指针的引用,直接赋给它就链接上了。
还不用像上面循环实现的那样去判断要连接到那边。

那大家思考一下,我们上面循环的方式,可以用引用吗?
不可以的,因为C++中的引用是不能改变指向的,引用一旦引用一个实体之后,就不能再引用其他实体
而这里递归的话,每次递归都相当于创建了一个新的引用,而不是改变上一层的引用的指向。

测试一下:

在这里插入图片描述

8. 删除(递归版本)

然后是删除:
在这里插入图片描述

8.1 思路分析

怎么实现呢?

那其实大致的思路还是一样的,从根结点开始判断,如果要删除的值大于根,转换为去右子树删,小于根,转换为去左子树删,等于,就进行删除,如果走到空,那就是找不到。
在这里插入图片描述
所以关键还是在于如何进行删除。

8.2 代码实现

那我们来分析一下:

其实还是我们上面分析的那三种情况:
左为空、右为空或者左右都不为空。
那这里我们用引用,写起来还是比较简便的。
先写一下左为空和右为空的情况,这两个比较好处理
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

然后看一下比较麻烦的左右都不为空的情况

我们之前非递归版本的实现是,找一个符合条件的结点替换它,然后把替换的结点删除掉
在这里插入图片描述
这里也可以用同样的方法,但我觉得比较简便的方法是这样的:
还是先找一个可以替代要删除结点的结点(左子树最大结点或右子树最小),以左子树最大结点替换为例
在这里插入图片描述
交换它们两个的值,然后删除左子树里面的8(key)这个结点就行了
在这里插入图片描述

测试一下:

在这里插入图片描述
没问题

bool EraseR(const K& key)
{
	return _EraseR(_root, key);
}

bool _EraseR(Node*& root, const K& key)
{
	if (root == nullptr)
		return false;
	if (key < root->_key)
	{
		return _EraseR(root->_left, key);
	}
	else if (key > root->_key)
	{
		return _EraseR(root->_right, key);
	}
	else
	{
		//删除
		Node* del = root;
		if (root->_left == nullptr)
		{
			root = root->_right;
		}
		else if (root->_right == nullptr)
		{
			root = root->_left;
		}
		else
		{
			Node* maxLeft = root->_left;
			while (maxLeft->_right)
			{
				maxLeft = maxLeft->_right;
			}
			swap(maxLeft->_key, root->_key);
			return _EraseR(root->_left, key);
		}
		delete del;
		return true;
	}
}

9. 其它相关成员函数的实现

如果我们想在相对搜索二叉树的对象进行拷贝构造可以吗?

在这里插入图片描述
是可以的,虽然我们没写,但是拷贝构造属于默认成员函数,编译器会自动生成,不过默认生成的只完成浅拷贝。
在这里插入图片描述
现在没有报错的原因是因为我们没写析构,如果有析构就会出问题,因为搜索二叉树涉及资源申请,这样如果是浅拷贝的话,在析构的时候就会对一块空间析构两次,所以就会出问题。
这都是我们之前学过的内容。

9.1 析构

那我们可以先来写一下析构。

那析构的话我们这里还是用递归来搞,也可以用循环,但是比较麻烦:

在这里插入图片描述
那实现一下Destory就行了,关于二叉树的销毁我们初阶也讲过,比较好的做法是后续销毁
在这里插入图片描述

那现在不出意外,有了析构我们的浅拷贝就要出错了

在这里插入图片描述
调式一下
在这里插入图片描述

9.2 构造和拷贝构造

那我们就来写一个深拷贝的拷贝构造,我们还是用递归来实现

那也比较简单,先序去拷贝创建就行了
在这里插入图片描述

那有了拷贝构造我们得实现一下构造函数:

在这里插入图片描述
因为现在有了拷贝构造,编译器就不会生成默认的构造函数了,那就需要我们自己写了
在这里插入图片描述
另外C++11有一个关键字——default,可以强制编译器生成默认构造,这个我们后面也会讲
在这里插入图片描述
那有了默认构造,我们下面给了缺省值,它走初始列表的时候就会用缺省值去初始化。
在这里插入图片描述
现在就可以了。

9.3 赋值重载

那赋值重载我们也搞一下吧:

跟我们之前玩的一样

BSTree<K>& operator=(const BSTree<K> t)
{
	swap(_root, t._root);
	return *this;
}

在这里插入图片描述

那关于搜索二叉树的实现差不多就到这里了

10. 完整代码

gitee

10.1 BSTree.h

#pragma once

template <class K>
struct BSTreeNode
{
	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;
public:
	bool Insert(const K& key)
	{
		if (_root == nullptr)
		{
			_root = new Node(key);
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		//走到这里cur为空,就是key应该插入的位置
		cur = new Node(key);
		//链接
		if (key < parent->_key)
			parent->_left = cur;
		if (key > parent->_key)
			parent->_right = cur;
		return true;
	}
	bool Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_key)
			{
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				cur = cur->_right;
			}
			else
			{
				return true;
			}
		}
		return false;
	}
	bool Erase(const K& key)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (key < cur->_key)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key > cur->_key)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				//key == cur->_key就是找到了,进行删除
				//1.左为空
				if (cur->_left == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_right;
					}
					else
					{
						if (cur == parent->_left)
							parent->_left = cur->_right;
						else
							parent->_right = cur->_right;
					}
					delete cur;
					cur = nullptr;
				}
				//2.右为空
				else if (cur->_right == nullptr)
				{
					if (cur == _root)
					{
						_root = cur->_left;
					}
					else
					{
						if (cur == parent->_left)
							parent->_left = cur->_left;
						else
							parent->_right = cur->_left;
					}
					delete cur;
					cur = nullptr;
				}
				else//左右都不为空
				{
					//这里选择右树的最小结点(最左边)替换
					Node* pminRight = cur;
					Node* minRight = cur->_left;
					while (minRight->_left)
					{
						pminRight = minRight;
						minRight = minRight->_left;
					}
					cur->_key = minRight->_key;
					//删除替换结点
					if (pminRight->_left = minRight)
					{
						pminRight->_left = minRight->_right;
					}
					else
					{
						pminRight->_right = minRight->_right;
					}
					delete minRight;
					minRight = nullptr;
				}
				return true;
			}
		}
		//找不到直接返回false
		return false;
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool FindR(const K& key)
	{
		return _FindR(_root, key);
	}
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
	bool EraseR(const K& key)
	{
		return _EraseR(_root, key);
	}
	/*BSTree()
		:_root(nullptr)
	{}*/
	BSTree() = default;
	~BSTree()
	{
		Destory(_root);
	}
	BSTree(const BSTree<K>& t)
	{
		_root = copy(t._root);
	}
	BSTree<K>& operator=(const BSTree<K> t)
	{
		swap(_root, t._root);
		return *this;
	}
protected:
	Node* copy(Node* root)
	{
		if (root == nullptr)
			return nullptr;

		Node* newRoot = new Node(root->_key);
		newRoot->_left = copy(root->_left);
		newRoot->_right = copy(root->_right);
		return newRoot;
	}
	void Destory(Node*& root)
	{
		if (root == nullptr)
			return;
		Destory(root->_left);
		Destory(root->_right);

		delete root;
		root = nullptr;
	}
	bool _EraseR(Node*& root, const K& key)
	{
		if (root == nullptr)
			return false;
		if (key < root->_key)
		{
			return _EraseR(root->_left, key);
		}
		else if (key > root->_key)
		{
			return _EraseR(root->_right, key);
		}
		else
		{
			//删除
			Node* del = root;
			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
				root = root->_left;
			}
			else
			{
				Node* maxLeft = root->_left;
				while (maxLeft->_right)
				{
					maxLeft = maxLeft->_right;
				}
				swap(maxLeft->_key, root->_key);
				return _EraseR(root->_left, key);
			}
			delete del;
			return true;
		}
	}
	bool _InsertR(Node*& root, const K& key)
	{
		if (root == nullptr)
		{
			root = new Node(key);
			return true;
		}
		if (key > root->_key)
		{
			return _InsertR(root->_right, key);
		}
		else if (key < root->_key)
		{
			return _InsertR(root->_left, key);
		}
		else
		{
			return false;
		}
	}
	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
			return false;
		if (key < root->_key)
		{
			return _FindR(root->_left, key);
		}
		else if (key > root->_key)
		{
			return _FindR(root->_right, key);
		}
		else
		{
			return true;
		}
	}
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		_InOrder(root->_left);
		cout << root->_key << " ";
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

10.2 BSTree.cpp

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include "BSTRee.h"

void BSTreeTest1()
{
	BSTree<int> t1;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e:a)
	{
		t1.Insert(e);
	}
	t1.InOrder();
	cout << t1.FindR(10) << endl;
	cout << t1.FindR(19) << endl;

}
void BSTreeTest2()
{
	BSTree<int> t1;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t1.Insert(e);
	}
	t1.InOrder();

	t1.Erase(10);
	t1.InOrder(); 
	t1.Erase(14);
	t1.InOrder();
	t1.Erase(13);
	t1.InOrder();
	t1.Erase(8);
	t1.InOrder();
	for (auto e : a)
	{
		t1.Erase(e);
		t1.InOrder();
	}
}
void BSTreeTest3()
{
	BSTree<int> t1;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t1.InsertR(e);
	}
	t1.InOrder();

	for (auto e : a)
	{
		t1.EraseR(e);
		t1.InOrder();
	}

}
void BSTreeTest4()
{
	BSTree<int> t1;
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto e : a)
	{
		t1.InsertR(e);
	}
	t1.InOrder();

	BSTree<int> t2=t1;
	t2.InOrder();

}
int main()
{
	BSTreeTest4();
	return 0;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/810858.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

七大经典比较排序算法

1. 插入排序 (⭐️⭐️) &#x1f31f; 思想&#xff1a; 直接插入排序是一种简单的插入排序法&#xff0c;思想是是把待排序的数据按照下标从小到大&#xff0c;依次插入到一个已经排好的序列中&#xff0c;直至全部插入&#xff0c;得到一个新的有序序列。例如&#xff1a;…

Doc as Code (3):业内人士的观点

作者 | Anne-Sophie Lardet 在技术传播国际会议十周年之际&#xff0c;Fluid Topics 的认证技术传播者和功能顾问 Gaspard上台探讨了“docOps 作为实现Doc as Code的中间结构”的概念。在他的演讲中&#xff0c;观众提出了几个问题&#xff0c;我们想分享Gaspard的见解&#x…

深入学习 Redis - 渐进式遍历 scan 命令、数据库管理命令

目录 前言 一、scan 命令 二、数据库管理命令 select dbsize flushdb / flushall 前言 之前我们所了解到的 keys * 是一次性把整个 redis 中所有的 key 都获取到&#xff0c;但是整个操作比较危险&#xff0c;可能会一下子的都太多的 key&#xff0c;阻塞 redis 服务器. …

NLP(六十三)使用Baichuan-7b模型微调人物关系分类任务

任务介绍 人物关系分类指的是对文本中的两个人物&#xff0c;在特定的关系列表中&#xff0c;判断他们之间的人物关系。以样本亲戚 1837年6月20日&#xff0c;威廉四世辞世&#xff0c;他的侄女维多利亚即位。为例&#xff0c;其中亲戚为人物关系&#xff0c;威廉四世为实体1&a…

vins调试的注意事项

1、摄像头的内参和畸变矫正系数 这个系数不对&#xff0c;没法做&#xff0c;因为下一步没法做对。这个会导致系统无法初始化。 2、对畸变的像素点&#xff0c;求得归一化坐标的方法 理解不同矫正模型的原理&#xff0c;确保矫正对了&#xff0c;得到z1平面的去畸变点。 3、摄…

python皮卡丘编程代码教程,用python打印皮卡丘

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;如何用print函数打印一只皮卡丘&#xff0c;用python如何打印丘比特之心&#xff0c;现在让我们一起来看看吧&#xff01;

CCL 2023 电信网络诈骗案件分类评测-第一名方案

1 任务内容 1.1 任务背景 2022年12月1日起&#xff0c;新出台的《反电信网络诈骗犯罪法》正式施行&#xff0c;表明了我国治理当前电信网络诈骗乱象的决心。诈骗案件分类问题是打击电信网路诈骗犯罪过程中的关键一环&#xff0c;根据不同的诈骗方式、手法等将其分类&#xff…

13个ChatGPT类实用AI工具汇总

在ChatGPT爆火后&#xff0c;各种工具如同雨后春笋一般层出不穷。以下汇总了13种ChatGPT类实用工具&#xff0c;可以帮助学习、教学和科研。 01 / ChatGPT for google/ 一个浏览器插件&#xff0c;可搭配现有的搜索引擎来使用 最大化搜索效率&#xff0c;对搜索体验的提升相…

【机器学习】Linear Regression

Model Representation 1、问题描述2、表示说明3、数据绘图4、模型函数5、预测总结附录 1、问题描述 一套 1000 平方英尺 (sqft) 的房屋售价为300,000美元&#xff0c;一套 2000 平方英尺的房屋售价为500,000美元。这两点将构成我们的数据或训练集。面积单位为 1000 平方英尺&a…

C++ 类和对象篇(零) 面向过程 和 面向对象

目录 一、面向过程 二、面向对象 三、两种编程思想的比较 四、C和C 一、面向过程 1.是什么&#xff1f; 是一种以解决问题的过程为中心的编程思想。即先分析出解决问题所需要的步骤&#xff0c;然后用函数把这些步骤一步一步实现。 2.为什么&#xff1f; 面向过程就纯粹是分析…

基于x-scan扫描线的3D模型渲染算法

基于x-scan算法实现的z-buffer染色。c#语言&#xff0c;.net core framework 3.1运行。 模型是读取3D Max的obj模型。 x-scan算法实现&#xff1a; public List<Vertex3> xscan() {List<Vertex3> results new List<Vertex3>();SurfaceFormula formula g…

SAP 自定义BADI增强点

应用场景 标准化代码中预留客制化部分&#xff0c;保证代码主体完整性&#xff0c;可以在预留增强位置预留两种类型的增强处理&#xff0c;其一为标准增强类型的&#xff0c;增强部分代码属于增加的逻辑&#xff0c;其二对于部分多样化的逻辑&#xff0c;使用优先执行默认逻辑&…

Java常用API:Object、Objects、包装类

Object类API toString 返回字符串类型 equals 默认比较的是地址 此时返回的是 false 可以在类中重写equals 方法 比较内容 如果内容一样就返回true clone 不能在测试类中用&#xff0c;必须在创建的类中重写克隆方法 还必须要有接口&#xff0c;说明这个对象有这个能力克隆 …

增量预训练baichuan-13b-chat遇到的那些坑

文章目录 前言资源deepspeed一、训练的坑二、推理的坑三、继续训练的坑总结前言 资源 单机两4090,如图 单卡24G,baichuan-13b-chat单卡推理需要至少26G,因此仅用一张卡,我们是无法加载百川13B的模型,所以,无论是推理还是训练,我们都必须并行! deepspeed 核心思想…

主干网络篇 | YOLOv8 更换主干网络之 VanillaNet |《华为方舟实验室最新成果》

论文地址:https://arxiv.org/pdf/2305.12972.pdf 代码地址:https://github.com/huawei-noah/VanillaNet 在基础模型的核心是“多样性即不同”,这一哲学在计算机视觉和自然语言处理方面取得了惊人的成功。然而,优化和Transformer模型固有的复杂性带来了挑战,需要转向简洁性…

Python-Python基础综合案例--数据可视化 - 地图可视化

版本说明 当前版本号[20230729]。 版本修改说明20230729初版 目录 文章目录 版本说明目录知识总览图Python基础综合案例--数据可视化 - 地图可视化基础地图使用案例效果视觉映射器 疫情地图-国内疫情地图案例效果实操设置全局配置选项 疫情地图-省级疫情地图案例效果实操 知…

spring拦截器 与统一格式

目录 前言模拟拦截器拦截器的实现原理什么是动态代理? 什么是静态代理静态代理与动态代理的区别两种常用的动态代理方式基于接口的动态代理基于类的动态代理 JDK Proxy 与 CGlib的区别 其他 统⼀访问前缀添加统⼀异常处理统⼀数据返回格式 前言 之前博客讲述了 , 关于SpringA…

Kotlin~Memento备忘录模式

概念 备忘录模式是一种行为型设计模式&#xff0c;用于捕获和存储对象的内部状态&#xff0c;并在需要时将对象恢复到之前的状态。 备忘录模式允许在不暴露对象内部实现细节的情况下&#xff0c;对对象进行状态的保存和恢复。 角色介绍 Originator&#xff1a;原发器&#x…

7.事件类型

7.1鼠标事件 案例-轮播图点击切换 需求&#xff1a;当点击左右的按钮&#xff0c;可以切换轮播图 分析: ①右侧按钮点击&#xff0c;变量&#xff0c;如果大于等于8&#xff0c;则复原0 ②左侧按钮点击&#xff0c;变量–&#xff0c;如果小于0&#xff0c;则复原最后一张 ③鼠…

Zotero ubuntu2023安装 关联 ubuntu文献翻译

一、准备下载的软件&#xff1a; Zotero | Downloads 1. Zotero-6.0.26_linux-x86_64.tar.bz2 下面是插件 zotfile-5.1.2-fx.xpi zotero-pdf-translate.xpi jasminum-v0.2.6.xpi 2.2.5 Tampermonkey 4.11.crx 所准备的文件&#xff0c;都已经在这个链接的压缩包下面 …