深入理解C++系列(15)——AVL树

⭐️博主: 此生决int-@CSDN博客
** 速胜派就是最大的投降派!!!**

**         🔥热门专栏🔥**

      深入理解 C++ 系列算法系列

      快速复习系列Java 速通系列


文章目录

上期回顾

上一篇我们主要学习了 如何使用map和set,了解了他们相关的接口,做了相关的一些算法题,那么今天,我们就来看看,怎么保证二叉搜索树的高度不会太高,达到logN的效率的呢?那要我们学完今天的AVL树就知道了!

AVL树

AVL树简介

1,AVL树概念:

简单来说就是,二叉搜索树里面任何一颗子树的左右子树高度差不超过1

名字由来:得名于它的发明者G. M. Adelson-Velsky和E. M. Landis是两个前苏联的科学家,

AVL树的实现

AVL树的结构

相比与我们之前实现的二叉搜索树,AVL树新增了:

1,指向父母的指针parent

2,平衡因子------bf(左右子树高度差,我们这里用右减左)

cpp 复制代码
template<class K, class V>
struct AVLTreeNode
{
    // 需要parent指针,后续更新平衡因子可以看到
    pair<K, V> _kv;
    AVLTreeNode<K, V>* _left;
    AVLTreeNode<K, V>* _right;
    AVLTreeNode<K, V>* _parent;
    int _bf; // balance factor
};

我们可以发现,AVL树的每颗子树的平衡因子只能为1,-1,0

insert插入函数的实现⭐️⭐️⭐️⭐️⭐️

插入的过程很简单:首先还是跟二叉搜索树一样,先找到插入位置;

代码:和之前二叉搜索树时的一样

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	//插入已经有的值就会返回false
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}
	Node* cur = _root;
	Node* parent= nullptr;
	while (cur)
	{
		if (kv.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

然后我们会发现,插入后会形成两种情况。

情况一是插入之后,它仍然是 AVL 树。

例如,在刚刚那副图里再插入一个11,仍然是AVV树

情况二是插入之后,它不满足 AVL 树的性质,这时候我们就要做出调整。

例如,插入13

好,我们一种情况一种情况来分析:

我们首先来想一下,我们要维护哪些东西:首先肯定是新增的那个平衡因子,还有父节点(parent),左右孩子,还有储存的值 kv。其中,这个平衡因子是比较难维护的。我们来单独看一下平衡因子怎么维护。

平衡因子的维护

首先,平衡因子是由右子树的高度减去左子树的高度得到的。

所以,如果一棵树在插入一个节点之后,它的左右子树高度都不变,那么它的平衡因子也不会改变 。所以,平衡因子肯定跟高度有关。所以,在插入一个节点之后,该节点所有祖先节点的平衡因子都有可能受到影响,我们都需要进行更新。

但是,我们观察可以得出一个结论:

如果插入之后,有一棵子树它的根节点的平衡因子变为了 0,那么它的所有祖先节点的平衡因子都不用继续更新了

证明:

插入之后,它的平衡因子变为了 0。那么,插入之前,它的平衡因子肯定是1或者 -1。在是一和 -1 的时候,肯定是左右两边有一边多了一个,新增的那个元素就插入在了少的那一边,抹平了那个差距,但整体它的树的高度是没有变的

所以,那棵子树的高度是没有变的。即:插入之后平衡因子变为 0 的那棵子树,它的高度肯定是不会变的

那么,对于插入节点之后,父母的平衡因子变为 1 或 -1 的这种情况:

我们知道:它插入之前肯定是 0,插入后变为 1 或 -1,那么它的高度肯定是增加了 1

那么,接下来我们只需要看它是它父母的左子树还是右子树,根据它是它父母的左子树还是右子树来更新它父母的平衡因子。

第三种情况:插入之后,一直往上更新的时候,父节点的平衡因子变为了 2 或者 -2。那么这个情况比较复杂,就要利用到旋转来解决

好,那么我们就可以把插入之后的平衡因子进行归纳分类:

平衡因子的几种情况:

1,插入后是0:

不用继续向上更新

2,插入后是1,-1
根据是父母的左子树还是右子树,来更新父母的平衡因子
3,插入后是2,-2
情况比较多,要通过旋转来解决

我们先把前两种情况的代码写出来:

cpp 复制代码
cur = new Node(kv);
if (kv.first>parent->_kv.first)
{
	parent->_right = cur;
	parent->_bf++;
}
else if (kv.first < parent->_kv.first)
{
	parent->_left = cur;
	parent->_bf--;
}
else
{
	assert(false);//防御性编程,理论上不可能走到这里
}
cur->_parent = parent;
cur->_bf = 0;
//更新平衡因子
// 根据父母的平衡因子来移动
//0,不用动
//1,-1,不管是1还是-1,肯定是0变过来的,然后,肯定该子树的高度+1了,所以,看父母是父母的左孩子还是右孩子
while (parent)
{
	if (parent->_bf == 0)
		break;
	else if (parent->_bf == 1 || parent->_bf == -1)
	{
		cur = parent;
		parent = parent->_parent;
		if (parent == nullptr)break;//爷爷为空,那么就是到根节点了,直接break
		if (parent->_left == cur)
		{
			parent->_bf--;
		}
		else if (parent->_right == cur)
		{
			parent->_bf++;
		}
		else
			assert(false);
	}

插入后,平衡因子为2和-2时

这里会分为四种情况,对应四种旋转方式,分别是:

  1. 左单旋
  2. 右单旋
  3. 左右双旋
  4. 右左双旋
    其中后面两个双旋就是上面两个单旋的组合,所以一定要先搞懂单选,再去看多选。搞懂单旋之后,双旋就会比较简单

右单旋

当一棵树它的左子树特别高(bf=-2) 的时候,它就会采用 单旋

下面这张图非常的关键!!

单从结果上来理解:

代码实现:

cpp 复制代码
//所有旋转的情景是,元素已经插入,然后,超级不平衡,即parent的平衡因子=2/-2
// 右单旋
void RotateR(Node* parent)
{
	//注意为空的几种情况
	// pParent为空
	// subLR 为空
	//
	Node* pParent = parent->_parent;
	Node* sub = parent;
	Node* subL = sub->_left;
	Node* subLR = subL->_right;
	sub->_left = subLR;
	if(subLR)//subLR可能为空,要特判
	subLR->_parent = sub;
	subL->_right = sub;
	sub->_parent = subL;
	if (pParent == nullptr)
	{
		_root = subL;
		//parent也要更新!!!
		subL->_parent = nullptr;
	}
	else if (pParent->_left == sub)
	{
		pParent->_left = subL;
		subL->_parent = pParent;//别忘了更新parent
	}
	else if (pParent->_right == sub)
	{
		pParent->_right = subL;
		subL->_parent = pParent;
	}
	else
		assert(false);
	//平衡因子更新
	subL->_bf = 0;
	sub->_bf = 0;
}

左单旋

与右单旋刚好相反,它的右子树特别高(bf=2) ,所以要进行 单旋。理解了右单旋,左单旋就很好理解了。

依旧是:把 parent 的右孩子作为新的根。

然后,parent 右孩子的左孩子裁剪下来,作为parent的右孩子,

最后,原来的 parent 作为新节点的左孩子。

左右双旋

顾名思义,"左右双旋"就是先进行一次左旋,再进行一次右旋

那么,我们就先来分析一下,到底是什么情况下要用单旋,什么情况下要用双旋。

其他两个同理:

左右单旋具体是怎么实现的呢?

简单来讲呢,双旋分为三种情况:

情况1:插入subLR的左子树

单从结果的角度来讲就是:

情况2:插入subLR的右子树(与情况一差不多)
情况3,特殊情况,h=0,即subLR就是插入节点

代码:

cpp 复制代码
// 左右双旋,即先左旋在右旋
void RotateLR(Node* parent)
{
	Node* sub = parent;
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;//先存储一下,
	RotateL(subL);
	RotateR(sub);
	//更新平衡因子
	//subLR->_bf = 0;//这个节点成为新的根了,那么,肯定是0
	//其他两个,要根据插入节点是subLR的左右节点来判断
	//不能再rotate后根据平衡因子判断,因为这里已经变了!!!!!!!!
	// if (subLR->_bf == -1)//也就是插入图示里面的e,也就是8的左边
	if (bf == -1)//也就是插入图示里面的e,也就是8的左边
	{
		sub->_bf=  1;
		subL->_bf = 0;
	}
	else if (bf == 1)
	{
		sub->_bf = 0;
		subL->_bf = -1;
	}
	else if (bf == 0)
	{
		sub->_bf = 0;
		subL->_bf = 0;
	}
	else
		assert(false);

	subLR->_bf = 0;//因为要以它为依据判断,所以,后更新

}

右左双旋(同理,会了左右就会右左)

情况1
情况2
情况3,特殊情况,h=0

insert完整实现代码

cpp 复制代码
// 插入
bool Insert(const pair<K, V>& kv)
{
	//插入已经有的值就会返回false
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}
	Node* cur = _root;
	Node* parent= nullptr;
	while (cur)
	{
		if (kv.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}
	cur = new Node(kv);
	if (kv.first>parent->_kv.first)
	{
		parent->_right = cur;
		parent->_bf++;
	}
	else if (kv.first < parent->_kv.first)
	{
		parent->_left = cur;
		parent->_bf--;
	}
	else
	{
		assert(false);//防御性编程,理论上不可能走到这里
	}
	cur->_parent = parent;
	cur->_bf = 0;
	//更新平衡因子
	// 根据父母的平衡因子来移动
	//0,不用动
	//1,-1,不管是1还是-1,肯定是0变过来的,然后,肯定该子树的高度+1了,所以,看父母是父母的左孩子还是右孩子
	while (parent)
	{
		if (parent->_bf == 0)
			break;
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			cur = parent;
			parent = parent->_parent;
			if (parent == nullptr)break;//爷爷为空,那么就是到根节点了,直接break
			if (parent->_left == cur)
			{
				parent->_bf--;
			}
			else if (parent->_right == cur)
			{
				parent->_bf++;
			}
			else
				assert(false);
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			//旋转
			if (parent->_bf == -2 && cur->_bf == -1)
			{
				RotateR(parent);
				//旋转后,不用向上更新了,
				break;
			}
			else if (parent->_bf == -2 && cur->_bf == 1)
			{
				RotateLR(parent); break;
			}
			else if (parent->_bf == 2 && cur->_bf == 1)
			{
				RotateL(parent); break;
			}
			else if (parent->_bf == 2 && cur->_bf == -1)
			{
				RotateRL(parent); break;
			}
			else
				assert(false);

		}
		else
			assert(false);
	}
	return true;
}

左单旋代码

cpp 复制代码
// 左单旋
void RotateL(Node* parent)
{
	Node* pparent = parent->_parent;
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;
	subR->_left = parent;
	parent->_parent = subR;
	if (pparent == nullptr)
	{
		_root = subR;
		//parent也要更新!!!
		subR->_parent = nullptr;
	}
	else if (pparent->_left == parent)
	{
		pparent->_left = subR;
		subR->_parent = pparent;
	}
	else if (pparent->_right == parent)
	{
		pparent->_right = subR;
		subR->_parent = pparent;
	}
	else
		assert(false);
	//更新平衡因子
	parent->_bf = 0;
	subR->_bf = 0;
}

右左双旋代码

cpp 复制代码
void RotateRL(Node* sub)
{
	Node* subR = sub->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;
	RotateR(subR);
	RotateL(sub);
	// 更新平衡因子
	if (bf == 0)
	{
		sub->_bf = 0;
		subR->_bf = 0;
	}
	else if (bf == 1)       // 新节点插入在 subRL 的右边
	{
		sub->_bf = -1;      // sub 变成了左子树,没有右孩子
		subR->_bf = 0;      // subR 左右平衡
	}
	else if (bf == -1)      // 新节点插入在 subRL 的左边
	{
		sub->_bf = 0;       // sub 左右平衡
		subR->_bf = 1;      // subR 只有右孩子
	}
	else
		assert(false);
	subRL->_bf = 0;
}

IsBalanceTree判断一颗树是不是AVL树

计算右子树高度,计算左子树高度,然后相减。

判断差值的绝对值是否小于 2,以及该差值是否等于平衡因子 BF即可。

代码:

cpp 复制代码
// 平衡检测辅助函数
bool _IsBalanceTree(Node* root)
{
	if (root == nullptr)return true;
	int left_height =_Height(root->_left);
	int right_height = _Height(root->_right);
	int bf = right_height - left_height;
	if (_IsBalanceTree(root->_left) && _IsBalanceTree(root->_right) && bf<2 && bf>-2)
	{
		//还要判断平衡因子
		if (root->_bf!= bf)
		{
			cout << "平衡因子错误:" << endl;
			return false;
		}
		return true;
	}
	return false;
}

高度函数怎么求?

以前学过,就是递归:先递归左子树的高度,再递归右子树的高度,然后再加 1

总结:

全是重点!

下期预告

红黑树

结语

**  本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**

**  也欢迎订阅我的**

深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++

算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线

快速复习系列:知识梳理、查漏补缺,考前冲刺必备

Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试


**  愿每一次敲下键盘,都比昨天更进一步!** **  愿每一行代码落下,都让未来多一种可能!**

相关推荐
一只旭宝1 小时前
预约系统版本2(pyhton+flask可视化版本)
服务器·数据库·c++·笔记·python·flask
筱璦1 小时前
PHP+ VUE3机构级股票分仓管理交易系统源码可出
开发语言·php·vue3·量化·股票分仓
爱奥尼欧1 小时前
10.C++ string 实现详解
java·开发语言·c++
骇客野人1 小时前
Java分布式任务调度方案(完整落地指南)
java·开发语言·分布式
JacksonMx1 小时前
Spring @Async 坑点 + 改造方案
java·开发语言
你怎么知道我是队长2 小时前
IPv6 地址完全解析:从结构到实战
开发语言·php
会周易的程序员2 小时前
软件接入大模型实现 Agent —— 从原理到 C++ 落地完全指南
c++·人工智能·物联网·架构·agent·工业协议·mcp
Dovis(誓平步青云)2 小时前
牛奶不能减成负数:冰箱库存的筛选、去重与不可变更新
android·java·开发语言
OPEN-F2 小时前
Python进阶教程:算法与数据结构入门
开发语言·python