C++--AVL树的插入,详解四种旋转规则(结尾附源代码链接)

前言

AVL树可以说是对二叉搜索树的优化,我们来看二叉树搜索树的下一面一种特殊情况:
在这里插入图片描述
当我们插入的数是上面的情况时,二叉树搜索树的特点就形同虚设了,这就相当于一个长度为N的单链表了,那么时间复杂度就是O(N)了。
那么AVL树为了处理这种情况的发生呢,就应运而生。

先来大致的说一下AVL是怎么处理才能避免出现这种情况的。AVL树主要是通过几种旋转的方式,控制了整颗树的任意结点的高度差不超过1,如果超过1,就会发生旋转。

AVL树的定义:
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树 。
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1。

AVL树的结点定义:
在上面提到了平衡因子是左右子树的高度差的绝对值,为了代码的实现的更明确,我们采用右子树的高度减去左子树的高度

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	pair<K, V> _kv;
	int _bf;//平衡因子(该结点的左右子树的高度差),右子树的高度减去左子树的高度

	AVLTreeNode(const pair<K, V>& kv)
		: _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	{}
};

因为在高度不满足条件的时候,涉及到旋转,而旋转就必须要有一个指针指向前一个结点,所以我们在定义结点的时候,将结点定义成了三叉链的结构。
再提一句就是关于这个pari<K, V> _kv。我们在实现的时候,完全可以根据自己的场景需要来决定是否使用pair。如果你仅仅需要一个值的话,是可以只提供一个模板参数的。

在正式的谈旋转之前,我们要先将能触发旋转的场景全部分析出来,主要的就是四种大场景。
其中左单旋和右单旋都是比较简单的。

左单旋

那么下面我们就用画图的方式来看一下如何触发左单旋。
在这里插入图片描述
上图的a b c矩形代表的是各种满足AVL的结点的情况,因为不同的情况有很多很多种,所以这里就以抽象图来观察我们的旋转的情况,这种抽象图也是大佬在观察了很多种情况总结出来的。

在上图中,当我们要在c结点插入一个新结点的时候(先不用考虑增加b的情况,这种情况是会引用发更为复杂的旋转的情况,下面会具体分析),c的高度就会增加1,变成h+1高度。c结点的高度的变化会直接影响其父亲结点的高度差,也就是60结点的高度差会由0变成1。60的高度的更新,又会影响其父亲结点的高度差的变化,当c的高度增加1的时候,增颗右子树的高度就会由h+1变成h+2,这时根节点30的左右子树的高度差就由1变成了2。
如下图所示:
在这里插入图片描述
此时根节点的高度差不满足绝对值小于1了,这时候的解决办法就是左旋转一下。
过程如下:

  • 将60的左子树给30做右子树。
  • 30做60的左子树。
    结果如下图所示:
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/512a29d0b3c3441cb9630
    11e4c0b9cd9.png)
    大家可以想一下为什么可以这样做?提示:二叉搜索树的特点是什么?

旋转完成之后,30和60的平衡因子需要再重新更新一下。
代码如下:
parent就是引发旋转的那个结点,上图中就是30。

void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		//1.
		parent->_right = subRL;
		subR->_left = parent;

		//2.更改parent指向
		if (subRL != nullptr)
			subRL->_parent = parent;

		Node* ppNode = parent->_parent;//在更改parent的_parent之前记录下parent的父结点
		parent->_parent = subR;
		//3.还需要进行判断,该parent是否是整个树的根节点还是子树的根节点
		if (ppNode == nullptr)
		{
			//parent是整个树的根节点
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
				ppNode->_left = subR;
			else
				ppNode->_right = subR;

			subR->_parent = ppNode;
		}
		
		//4.更新平衡因子
		subR->_bf = parent->_bf = 0;
	}

大家把变量名带上再画一张旋转的图就更容易理解代码的逻辑。

右单旋

右单旋其实和左单旋可以说是完全对称的结构,下面给出右单旋的所示图,过程就不再解释了,和左单旋并没有什么本质上的区别。
在这里插入图片描述
旋转完成之后,记得更新平衡因子的值。
以下是代码:

void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//1.
		parent->_left = subLR;
		subL->_right = parent;

		//2.更改parent指向
		if (subLR != nullptr)
			subLR->_parent = parent;

		Node* ppNode = parent->_parent;
		parent->_parent = subL;

		//3.还需要进行判断,该parent是否是整个树的根节点还是子树的根节点
		if (ppNode == nullptr)
		{
			//parent是整个树的根节点
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if(ppNode->_left == parent)
				ppNode->_left = subL;
			else
				ppNode->_right = subL;

			subL->_parent = ppNode;
		}
		//4.更新平衡因子
		subL->_bf = parent->_bf = 0;
	}

下面是两种单旋图的所示图,放在一起对比大家再来看,是真的没什么本质的区别:
在这里插入图片描述

左右双旋

接下来的双旋的场景,说难也不难,说简单也不简单哈哈!
还是同样的我们以抽象图来画图,这里提一句大家可以将抽象图画成具体的实例图去走旋转,只不过实例图的场景实在是太多,不太适合拿出来举例子,也不具有说服力,这四种旋转的抽象图就是包含了各种场景的四种分类。
如果不理解直接记住也行的。

还是先给出一颗AVL树,如下所示:
在这里插入图片描述
此时如果我们在60结点的左子树或者右子树下插入新的结点,引发的高度差变化就如下图所示:
在b或者c插入引发的是同一种双旋,但是虽然是同一种双旋,但是会导致最后的更新的平衡因子不同,所以在进行旋转之前我们要事先记录下,60结点的平衡因子,在后序的更新平衡因子的时候,我们会用到这一点
在这里插入图片描述
这时候的根节点的做右子树的高度差不再满足条件,就会触发旋转,这种场景仅仅一次单旋肯定是没法解决的,所示我们就需要双旋了。

第一次单旋:先对30结点进行左单旋,结果如下图所示:
在这里插入图片描述
第二次单旋:对90结点进行右单旋,结果如下图所示:
在这里插入图片描述
左旋和右旋的规则上面已经介绍过了,可以看出,双旋也就是两次单旋组合的,只不过在最后的旋转完成之后,需要对一些结点的平衡因子进行更新,更新平衡因子和单旋的时候不一样,所示在实现双旋的代码的时候,对于旋转,只需要复用之前写的单旋的代码,自己最后写一下关于平衡因子的更新即可。

代码如下:

void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;//根据这个结点的平衡因子来确定新形成的AVL树的平衡因子

		RotateL(parent->_left);
		RotateR(parent);

		//无论那种情况,subL(根)的平衡因子都是0
		subLR->_bf = 0;
		if (bf == 0)
		{
			parent->_bf = subL->_bf = 0;
		}
		else if (bf == -1)
		{
			//结合画抽象图来看平衡因子怎么更新!!!!!!!!!!!!!!!!!--易错点
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

右左双旋

右左双旋和左右双旋的关系呢,和左单旋和右单旋的关系是一样的,也是对称的关,只不过最后的平衡因子的更新还是需要根据画图来进行判断。

以下是右左双旋的过程示意图:
在这里插入图片描述
代码:

void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(parent->_right);
		RotateL(parent);

		subRL->_bf = 0;
		if (bf == 0)
		{
			parent->_bf = subR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
		}
		else if (bf == 1)
		{
			parent->_bf =-1;
			subR->_bf = 0;
		}
		else
			assert(false);
	}

左右双旋和右左双旋的对比示意图:
在这里插入图片描述

检查是否这颗树是否是AVL树

最后我们需要写一个函数,来检查用我们上面实现出来的 AVL树是满足要求的,即是一颗正确的AVL树。
值得一提的是,我们不能使用平衡因子来检查这颗树是否是正确的AVL树,因为平衡因子的更新也是我们自己写的,如果说因为我们自己的写的平衡因子有错误,而没有检查出来这颗树是有问题的,那么就无法保证改树是AVL树了。

思路如下:
既然平衡因子用不了,那么我们可以考虑直接计算出每个结点的左右子树的高度差,直接通过高度差来判断每个结点的高度差是否小于2。如果说不满足,那么就是我们实现的有问题,如果全部都检查完了,就说明我们实现的代码是能够构造出一颗正确的AVL树的。

代码如下:

bool isBalance()
	{
		return _isBalance(_root);
	}
private:
	bool _isBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		int leftHT = Height(root->_left);
		int rightHT = Height(root->_right);
		int diff = rightHT - leftHT;

		if (diff != root->_bf)
		{
			cout << root->_kv.first << "平衡因子异常" << endl;
			return false;
		}
		//所有的结点都满足AVLTree这颗树才是AVLTree
		return abs(diff) < 2
			&& _isBalance(root->_left)
			&& _isBalance(root->_right);
	}
	int Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		return max(Height(root->_left), Height(root->_right)) + 1;
	}

测试用例及结果:
在这里插入图片描述
测试函数返回结果为真,结果正确。!

以下是完整的AVL树代码链接:
https://gitee.com/WXK-Tom/c-data-structure/tree/master/AVLTree/AVLTree

欢迎大家评论留言!

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南山忆874

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值