【C++进阶】AVL树

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


前言

搜索二叉树章节,我们知道二叉搜索树可能会失去平衡(退化成单支树),造成搜索效率低落的情况,时间复杂度会退化成O(N)(效率没有保障)。因此,一些大佬在搜索二叉树的基础上增加了平衡的性质,于是就诞生了AVL树、红黑树等平衡树。

一、AVL树的概念

为了解决二叉搜索树的缺陷,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1,即可降低树的高度,确保整颗树的深度是O(logN)

因此,如果二叉搜索树具备以下性质

  1. 它的左右子树都是AVL
  2. 右子树的高度 - 左子树的高度的绝对值不超过1(平衡因子)

在这里插入图片描述

那么这颗搜索二叉树就是一颗AVL树。

二、AVL树的定义

AVL树在原二叉搜索树的基础上添加了平衡因子(Balance factor),简写: bf,以及用于快速向上调整的父亲指针parent(为什么定义指针变量parent,在插入部分会介绍到),所以AVL 树是一个三叉链结构

在这里插入图片描述

至于AVLTree类中,成员变量只需要创建一个 根节点_root即可

在这里插入图片描述

注意: 本篇博客规定平衡因子差值为右 - 左。当然左 - 右也是可以的,根据自己的个人习惯。

三、如何更新平衡因子

在这里插入图片描述

通过上图,我们可以发现一个结论: 左边多一个结点,其祖先的路径上的平衡因子_bf--

在这里插入图片描述

同理的,右边多一个结点,其祖先的路径上的平衡因子_bf++

【总结】

  • 左边多一个结点,其祖先的路径上的平衡因子_bf--
  • 右边多一个结点,其祖先的路径上的平衡因子_bf++

四、如何控制平衡

将结点插入以后,我们需要做的就是控制平衡

因此,就有以下3种情况

  1. parent的平衡因子为0时,说明插入的结点已经把矮的那边给补上了,那么就已经绝对平衡了,没必要再沿着祖先向上更新

在这里插入图片描述

  1. parent的平衡因子为1或者-1,就要沿着祖先的路径向上检查是否要更新。(祖先可能会不平衡)

在这里插入图片描述

  1. parent的平衡因子为2或者-2说明不平衡解决方法:对parent所在的子树进行旋转。(具体后面再谈)

根据以上分析,我们就可以写出大致的AVL插入的框架

在这里插入图片描述

解答为需要定义parent指针变量

在这里插入图片描述

因此,定义parent指针变量就是为了快速找到某结点的父亲,从而快速更新平衡因子以及快速判断是否需要进行旋转操作,减低了遍历子树来找到父结点的时间

五、插入操作(重点)

旋转需要注意的问题

  1. 还是需要保持它是一颗具有搜索树的性质(左子树比根小,右子树比根大)

  2. 让它变成平衡树,且减低这个子树的高度

5.1 左单旋

在这里插入图片描述

左单旋是为了处理当某个结点的右子树过深而导致失衡的情况(parent->_bf == 2 && cur->_bf == 1。具体步骤如下:

  1. cur的左结点作为parent的右结点

  2. 再将parent结点作为cur的左结点

  3. 修改父亲关系

    • cur左结点的父亲就是parent
    • parent结点的父亲就是cur
  4. 最后要考虑cur是否是以子树形成存在的,还是它就是个_root

【代码实现】

在这里插入图片描述

【左旋转代码实现】

在这里插入图片描述

5.2 右单旋

右单旋本质上和左单旋一样,有种对称的感觉~
在这里插入图片描述
右单旋是为了处理当某个节点的左子树过深而导致失衡的情况(parent->_bf == -2 && cur->_bf == -1。具体步骤如下:

  1. cur的右结点作为parent的左结点

  2. 再将parent结点作为cur的右结点

  3. 修改父亲关系

    • cur的右结点的父亲就是parent
    • parent结点的父亲就是cur
  4. 最后要考虑cur是否是以子树形成存在的,还是它就是个_root

【代码实现】

在这里插入图片描述

【右单旋代码实现】

在这里插入图片描述

5.3 双旋之左右双旋

在这里插入图片描述

从上图A样例发现:parent的左子树高,因此很容易可以想到右单旋来控制平衡。但是,通过图B发现,右单旋还是解决不了问题。

那么,如果是以上这种 折线型不平衡的情况,则要使用双旋来解决

左右双旋是为了处理parent的左子树cur的右子树过深而导致失衡的情况(parent->_bf == -2 && cur->_bf == 1。具体步骤如下:

  • 先对cur结点进行左旋操作(因为cur右子树过深)
  • 最后对parent结点进行右旋操作

我们可以根据以上步骤来验证

在这里插入图片描述

通过以上分析,有的人可能会旋转代码复用,几行代码就搞定了。

如果这样写就错了,**如果先左旋的话,左旋函数会自动将curparent的平衡因子置为0,然而左旋后,还要通过右旋才能平衡,因此 需要考虑旋转后平衡因子的情况:

  • curRight的平衡因子为0时(没有孩子),左右双旋后parentcurcurRight平衡因子都为0
    在这里插入图片描述

  • curRight的平衡因子为-1时(有左孩子),左右双旋后parentcurcurRight平衡因子分别为100

在这里插入图片描述

  • curRight的平衡因子为1时(有右孩子),左右双旋后parentcurcurRight平衡因子分别为0-10

    在这里插入图片描述

【代码实现】

在这里插入图片描述

【左右双旋代码实现】

在这里插入图片描述

5.4 双旋之右左双旋

在这里插入图片描述

右左双旋是为了处理parent结点的右结点cur的左子树过深而导致失衡的情况(parent->_bf == 2 && cur->_bf == -1。具体步骤如下:

  1. 先对cur结点进行右旋操作(cur左子树过深)
  2. 最后再对parent结点进行左旋操作

右左旋和左右旋类似,手撕代码之前同样需要考虑旋转后平衡因子的情况

  • curLeft的平衡因子为0时(没有孩子),右左双旋后parentcurcurLeft平衡因子都为0

在这里插入图片描述

  • curLeft的平衡因子为-1时(有左孩子),右左双旋后parentcurcurLeft平衡因子分别为都为010

在这里插入图片描述

  • curLeft的平衡因子为1时(有右孩子),右左双旋后parentcurcurLeft平衡因子分别为都为-100

在这里插入图片描述

【代码实现】

在这里插入图片描述

【右左双旋代码实现】

在这里插入图片描述

六、 AVL树的验证

平衡因子反映的是左右子树高度之差(本篇博客是:右子树 - 左子树)。通过计算出左右子树高度之差并与当前节点的平衡因子进行比对,如果发现不同,则说明 AVL 树非法。

注意:如果当前节点的 平衡因子 取值范围不在[-1, 1]内,也可以判断非法

// 验证AVL树
int Height(Node* root)
{
	if (root == nullptr)
	{
		return 0;
	}
	int leftHeight = Height(root->_left);
	int rightHeight = Height(root->_right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

bool IsBalance()
{
	return IsBalance(_root);
}

bool IsBalance(Node* root)
{
	if (root == nullptr)
	{
		return true;
	}
	int leftHeight = Height(root->_left);
	int rightHeight = Height(root->_right);

	if (rightHeight - leftHeight != root->_bf 
		|| root->_bf < -1 
		|| root->_bf > 1)
	{
		cout << "平衡因子异常:" << root->_key.first << "->" << root->_bf << endl;
		return false;
	}

	return abs(rightHeight - leftHeight) < 2
		&& IsBalance(root->_left)
		&& IsBalance(root->_right);
}

通过一段简单的代码,随机插入1w个节点,判断是否合法

#include "AVL.h"
#include <vector>
int main()
{
	const int N = 10000;
	vector<int> v(N);
	srand((size_t)time(NULL));

	for (int i = 0; i < N; i++)
	{
		v.push_back(rand());
	}

	AVLTree<int, int> t;
	for (auto x : v)
	{
		t.insert(make_pair(x, x));
		cout << "Insert:" << x << "->" << t.IsBalance() << endl;
	}
	return 0;
}

当打印出来的结果全为1(表示真),那么它就是一个AVL

七、总结

AVL 树是一棵 绝对平衡 的二叉树,对高度的控制极为苛刻,稍微有点退化的趋势,都要被旋转调整,这样做的好处是 严格控制了查询的时间,查询速度极快,时间复杂度为 logN。而对于删除,大家不用担心,因为在面试的时候只会考察AVL树的插入操作hh

这是本篇博客的相关代码:代码仓库。

  • 25
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值