AVL树的删除操作 --- Part two

序言

Part one 部分我们了解到了AVL树的定义,AVL在进行结点的调整(如插入,删除)时,会自适应的去调节树的高度,使左右高度的最大差值的绝对值不超过1。同时我们也介绍了调节的四个方法 — 左旋,右旋,左右旋,右左旋,他们具体的使用场景以及代码实现。这篇文章,我们将详细的讲解AVL树是如何进行删除操作的,但在这之前,大家还记得搜索二叉树是如何进行删除操作的吗❓


1. BSTree的删除操作 😉

我们在 基于二叉搜索树(BSTree) 实现 Map👈 这篇文章中详细地讲解了删除操作的过程,在这里我们就总结一下三种情况:

1.1 叶子结点的删除

最简单的的删除操作,但是需要注意该叶子节点是否是根节点

  • 删除前先判断是否为根节点
  • 不是根节点,直接删除;是根节点记得 root 置空,避免悬空指针

1.2 只有一个子节点

只需要将父节点连接该节点的子节点,也需要判断是否是根节点

  • 删除前先判断是否为根节点
  • 是根节点,将根节点指向该节点的子节点,删除该节点
  • 不是根节点,将父节点指向该节点的子节点,删除该节点

1.3 有两个子节点

直接删除该节点是比较困难的,我们使用的方法是将该节点的值和该节点的子节点的值交换,之后删除子节点,但是交换也是有讲究的,替换后是需要该树依旧满足搜索二叉搜索树的性质;我们一般替换的方法有两种,一是左树的最大值,二是右树的最小值,并且删除

  • 将该结点的值和子节点的值交换
  • 删除子节点,该子节点一定是前两个情况的其中一种

这就是二叉搜索树的删除操作,一定要分清楚是哪一种情况,选择合适的方式很重要。


2. AVL树的删除操作

2.1 平衡因子的更新方式

AVL树和搜索二叉树唯一的不同就是,前者时时刻刻需要关注平衡因子的值。当我们删除结点时,平衡因子肯定会受到影响,所以我们需要对其进行更新。
在这里我们不需要分三种情况来更新,我们只需要向上遍历更新即可,所以我们必须要时刻弄明白,变化的子树是父节点的左子树还是右子树,我们对平衡因子的定义是 最高的右子树与最高的左子树高度之差,所以更新的方式为 删除的位置在右子树则减一,反之加一。

2.2 旋转方式的判别

在插入操作时,我们的旋转方式的判别可以总结为:

  • cur 的平衡因子为 1,parent 的平衡因子为 2,进行左旋操作
  • cur 的平衡因子为 -1,parent 的平衡因子为 -2,进行右旋操作
  • cur 的平衡因子为 -1,parent 的平衡因子为 2,进行先右旋再左旋操作
  • cur 的平衡因子为 1,parent 的平衡因子为 -2,进行先左旋再右旋操作

删除操作稍显不同,他仍然需要关注 parent 的平衡因子,但是他 不再关注 cur 的平衡因子,而是关注 cur 兄弟的平衡因子。 就比如:
在这里插入图片描述

在这里如果我想要删除 8 这个值,删除之后可以发现 cur 的平衡因子变成了 0,parent 结点的平衡因子变成了 -2,不符合我们上述的任意一个条件。但是你看 brother 这个结点的平衡因子变成了 -1,根据条件推断是需要执行右旋操作,符合该情况。

该如何理解,删除时旋转方式的判断要依赖于兄弟节点呢🌀? 你可以这样理解,彼消此长 — 我自己这棵树变矮了,相对而言,我兄弟那棵树就变高了嘛。 变高了,肯定是需要判断是否需要旋转呀。

2.3 更新的结束条件

在进行插入操作时,我们的平衡因子跟新结束条件为 直到我们进行了一次旋转操作,父节点的平衡因子更新为 0。 但是对于删除操作来说,进行一次旋转操作可能还是不够的,有可能的需要多次旋转操作,就比如:
在这里插入图片描述
这里就需要两次旋转操作才能达到平衡。

那删除操作后,更新的结束条件是什么呢❓ 可以总结为:

  • 当更新的父节点的原始平衡因子为 0 时,这个需要和插入的结束条件区分开,前者为更新前为 0,后者是更新后为 0
  • 一直更新,直到根节点的平衡因子完成更新
  • 当旋转操作时,兄弟节点的平衡因子为 0

我们在这里着重解释第一点和第三点:
第一点:
在这里插入图片描述
就比如在这里需要删除 2 / 4,当对父结点 3 完成平衡因子的更新后,就完成了。
在这里解释一下,结点 3 的初始平衡因子为 0。根据平衡因子的定义,我们可以理解为 左子树的最大高度是等于右子树的最大高度。 现在左子树的高度或者是右子树的高度变矮了,但是对于上层结点来说,最大高度其实还是不变的呀!!!因为影响平衡因子的只能时最大高度的改变,现在虽然其中一棵树的高度变了,但是另一棵依旧没变!
对于图上来说,现在删除 2 结点,左子树原来的最大高度为 3,现在还是 3,该树为 534。

第二点:
这个也不难理解,我们一直都说,尝试去画图理解树,只要图能画出来,就一定可理解:
在这里插入图片描述
同一个图,只是删除的节点不同,在这里我们需要删除 6,父节点的平衡因子变为 -2,兄弟节点的平衡因子是 0,该怎么办呢?
答案是还是右旋,只是说这个右旋过后,平衡因子的改变和传统的不一样:
在这里插入图片描述
在传统的右旋操作后,parent,cur 的平衡因子都会变成 0 。但是在这里分别变成 -1, 1。 (左旋类似)
这里的原因和上面的差不多,普通的右旋操作会让整棵树的最大高度减一,但是该右旋最大高度不变。


3. 代码实现

光说不实现肯定是不可以的,现在我们尝试实现:

在这里记录并更新平衡因子

	// 记录并更新平衡因子
	int old_bf = parent->_bf;
	if (parent->_left == child) { parent->_bf++; }
	else { parent->_bf--; }

如果初始的平衡因子是 0,则直接退出:

	// 该节点原来的的平衡因子为 0,直接退出
	if (old_bf == 0) { break; }

之后如果不是 0,则判断是否旋转,以及旋转的方式(别忘了特殊的旋转,兄弟节点平衡因子为 0)

// 判断是否需要旋转,以及旋转方式:
if (parent->_bf == 2 || parent->_bf == -2) {
				// 如何旋转取决于,另一边孩子
				// 左旋
				if (parent->_left == child) {
					if (parent->_bf == 2 && parent->_right->_bf == 1) {
						Node* subR = parent->_right;
						RotateL(parent);
						// 更新结点
						parent = subR->_parent;
						child = subR;
						continue;
					}
					// 先右旋再左旋
					else if(parent->_bf == 2 && parent->_right->_bf == -1){
						Node* subRL = parent->_right->_left;
						RotateRL(parent);
						// 更新结点
						parent = subRL->_parent;
						child = subRL;
						continue;
					}
					// 特殊情况为 0,这种情况可以直接旋转后退出
					else if (parent->_bf == 2 && parent->_right->_bf == 0) {
						Node* subR = parent->_right;
						RotateL(parent);
						// 更新平衡因子
						subR->_bf = -1;
						subR->_left->_bf = 1;
						break;
					}
				}
				else {
					// 右旋
					if (parent->_bf == -2 && parent->_left->_bf == -1) {
						Node* subL = parent->_left;
						RotateR(parent);
						// 更新结点
						parent = subL->_parent;
						child = subL;
						continue;
					}
					// 左旋再右旋
					else if (parent->_bf == -2 && parent->_left->_bf == 1) {
						Node* subLR = parent->_left->_right;
						RotateLR(parent);
						// 更新结点
						parent = subLR->_parent;
						child = subLR;
						continue;
					}
					// 特殊情况为 0,这种情况可以直接旋转后退出
					else if (parent->_bf == -2 && parent->_left->_bf == 0) {
						Node* subL = parent->_left;
						RotateR(parent);
						// 更新平衡因子
						subL->_bf = 1;
						subL->_right->_bf = -1;
						break;
					}
				}
}

如果不满足上述两种情况,则更新结点,向上循环

	// 更新节点
	child = parent;
	parent = parent->_parent;

4. 总结

AVL树的内容就结束了。可以看到通过旋转操作,AVL树可以将查询效率始终控制在 O(log n)。但在 C++ STL 中却并没有使用该树来构造 map,set,而是使用红黑树。这位更是重量级,他的维护成本更低,删除效率更高,那他背后又是什么样的逻辑呢?👀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值