【高阶数据结构】AVL树详解(图解+代码)

news2024/11/24 17:17:54

文章目录

  • 前言
  • 1. AVL树的概念
  • 2. AVL树结构的定义
  • 3. 插入(仅仅是插入过程)
  • 4. 平衡因子的更新
    • 4.1 为什么要更新平衡因子?
    • 4.2 如何更新平衡因子?
    • 4.3 parent更新后,是否需要继续往上更新?
    • 4.4 平衡因子更新代码实现
  • 5. AVL树的旋转
    • 5.1 新节点插入较高右子树的右侧---右右:左单旋
      • 什么情况要进行左单旋
      • 如何进行左单旋
      • 左单旋代码实现
      • 什么时候调用左单旋
    • 5.2 新节点插入较高左子树的左侧---左左:右单旋
      • 什么情况要进行右单旋
      • 如何进行右单旋
      • 右单旋代码实现
      • 什么时候调用右单旋
    • 5.3 新节点插入较高左子树的右侧---左右:先左单旋再右单旋(左右双旋)
      • 什么情况进行左右双旋
      • 如何进行左右双旋
      • 左右双旋代码实现
      • 什么时候调用左右双旋
    • 5.4 新节点插入较高右子树的左侧---右左:先右单旋再左单旋(右左双旋)
      • 什么情况进行右左双旋
      • 如何进行右左双旋
      • 右左双旋代码实现
      • 什么时候调用右左双旋
    • 5.5 总结
  • 6. AVL树的测试
    • 6.1 验证其为二叉搜索树
    • 6.2 验证其为平衡树
    • 6.3 判断平衡因子的更新是否正确
    • 6.4 大量随机数构建AVL树进行测试
  • 7. 查找
  • 8. AVL树的删除(了解)
  • 9. AVL树的性能
  • 10. 源码
    • 10.1 AVLTree.h
    • 10.2 Test.cpp

前言

前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现。
这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

那这篇文章我们就重点来学习一下平衡搜索二叉树——AVL树

1. AVL树的概念

二叉搜索树虽可以提升查找的效率,但如果数据有序或接近有序时二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
在这里插入图片描述

因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:

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

那大家思考一个问题:为什么是每个结点左右子树高度之差的绝对值不超过1,为什么不能是两边一样高,高度差为0呢?

🆗,如果能达到左右子树完全一样高固然是最好的,但是关键在于有些情况不可能实现两边绝对平衡!
比如
在这里插入图片描述
两个结点、4个结点的情况,当然肯定不止这些。大家看这种情况能实现两边完全平衡吗?
是不行的,无法达到完全平衡。

所以,什么是平衡二叉树呢?

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

  1. 它的左右子树都是AVL树
  2. 左右子树高度之差(简称平衡因子,一般是右子树-左子树的高度差,当然左-右也可以)的绝对值不超过1(-1/0/1)
    ps:图中每个结点旁边的数字就是其对应的平衡因子在这里插入图片描述
    如果一棵二叉搜索树是高度平衡的(即满足任何一个结点的平衡因子都在[-1, 0, 1]这个范围内),它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索的时间复杂度O( l o g 2 n ) log_2 n) log2n)

2. AVL树结构的定义

那我们这里以KV模型的结构来讲解,当然本质都是一样的

首先我们来写一下结点的结构

template <class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K,V>* _right;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	int _bf;//balance factor(平衡因子)

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

这里我们给结点增加一个_parent指针指向它的父亲结点,方便我们后续进行某些操作,当然带来方便的同时我们也需要去维护每个结点的_parent指针,相应也带来了一些麻烦。
这个后面我们实现的时候大家就会体会到。

然后AVL树的结构

template <class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	//成员函数
private:
	Node* _root = nullptr;
};

那然后我们来写一下插入吧

3. 插入(仅仅是插入过程)

AVL树就是在二叉搜索树的基础上引入了平衡因子来控制树的相对平衡,因此AVL树也可以看成是二叉搜索树。

所以插入的逻辑其实跟搜索二叉树是一样的,不同的地方在于平衡二叉树插入之后如果整棵二叉树或者其中某些子树不平衡了我们要对插入的结点进行调整使得它重新变的平衡,那这个我们后面单独讲。

由于插入的逻辑我们之前已经讲过了,所以这里我就直接上代码了(这里我们选择非递归)

不过需要注意的是我们这里插入新结点之后还要链接_parent指针。

bool insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				ccur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		//链接父亲指针
		cur->_parent = parent;
		//更新平衡因子
		//...
		return true;
	}

大家看着代码再过一遍这个插入的过程。

那现在我问大家,AVL树的插入写到这里就完了吗?
🆗,如果是普通的搜索树,这就完事了,但是,对于平衡搜索二叉树来说,还远远没有结束。

4. 平衡因子的更新

为了实现平衡二叉树,我们引入了一个新的概念,不知道大家还记不记得是啥?

🆗,就是我们上面提到的平衡因子。
再来回顾一下什么是平衡因子?
一个结点的平衡因子就是它的左右子树的高度差,一般是右子树减左子树的高度(我们这里的讲解也统一以右子树-左子树的高度作为平衡因子)。

4.1 为什么要更新平衡因子?

那大家想一下:我们在AVL树中插入了一个新结点之后,会不会影响到树中结点的平衡因子?

毋庸置疑,这当然是会的!
因为一旦插入了新的结点,整棵树的高度或者某些子树的高度必然会发生变化,那树的高度发生变化,必然会影响与之关联的结点的平衡因子。

所以,插入了新结点之后,导致某些树的高度发生变化,我们要更新平衡因子。

🆗,那平衡因子会变化,这没啥说的,但是为啥变化了我就得更新呢?不更新行不行?

答案是不行。
为什么呢?
因为上面我们说了,如果一棵二叉搜索树是AVL树,那么它必须满足任何一个结点的平衡因子都在[-1, 0, 1]这个范围内。
而现在插入新结点会导致平衡因子变化,那么更新之后,某些结点的平衡因子可能就不在[-1, 0, 1]这个正常范围内了。
那他就不是一棵AVL树了,所以我们才要更新平衡因子,以此来判断这个树还是否是一棵AVL树。
如果不是了,即有结点的平衡因子不在正常范围内了,那这棵树的平衡就受到影响了,那我们就需要对新插入的结点进行调整,使他变回AVL树。
当然如果插入之后平衡没有受到影响,就不需要调整了。

那调整结点的事,我们后面再说,现在先谈一谈,插入新结点后,如何更新平衡因子!

4.2 如何更新平衡因子?

那首先大家思考一个问题,插入一个新结点之后,可能会影响到哪些结点的平衡因子?

是不是影响的肯定是它的祖先啊。
因为新插入的结点在它祖先的子树上,那它祖先的子树高度发生变化,平衡因子必然也会发生变化。
但是会影响所有的祖先吗?
不一定!可能只影响一部分。
比如:在这里插入图片描述
在这里插入图片描述
所以具体影响了几个祖先要根据具体情况具体分析。

那既然要更新,我就来研究一下更新的规律:

那这个规律呢,其实也很容易得出:
因为平衡因子的计算是右子树高度-左子树高度嘛。
所以,对于新结点的父亲来说:

  1. 如果插入在了右子树,那么父亲的平衡因子就要++
  2. 如果插入在了左子树,那么父亲的平衡因子就要- -


在这里插入图片描述
这时候我们的parent指针的作用就体现出来了

4.3 parent更新后,是否需要继续往上更新?

那父亲结点的平衡因子更新完之后,还要不要继续往上更新呢?

首先parent肯定要更新,因为插入之后它的子树的高度变了。
所以大家先想一下,什么情况下parent更新完之后还要继续往上更新parent的祖先?
🆗,是不是取决于parent所在的这棵子树的高度有没有发生变化啊。

  1. 如果插入之后parent这棵子树的高度没有变化,那就不会影响parent再往上结点(即parent的祖先)的平衡因子,就不需要往上继续更新了
    在这里插入图片描述
  2. 如果插入之后parent这棵子树的高度发生了变化,那parent的平衡因子更新完成后就需要继续往上更新
    在这里插入图片描述

那我们分析一下其实分为这三种情况:

  1. 如果parent的平衡因子更新之后为1或-1,则parent这棵树的高度发生变化,需要继续向上更新

为什么呢?为什么parent的平衡因子变成1或-1,它的高度就变了呢?

我们刚才是不是分析过,插入一个新结点,它的parent的平衡因子是怎么变化的,是不是要么-1,要么+1啊。
那它现在更新之后变成了1或者-1,能够说明什么?
是不是说明它更新之前的平衡因子一定是0啊,0的话说明他两边高度是平衡的,而现在插入之后变为1或-1,说明右边或者左边高了,因此高度肯定是变化了,那就要继续往上更新。
那继续往上更新是不是又是同样的逻辑啊(我们只需将结点往上走,下次循环自然会进行同样的处理,后面代码实现出来大家会更清晰)。

那可能是-2或者2加一减一之后变成-1或1啊?

不可能,因为AVL树的平衡因子的范围都是在[-1, 0, 1]内的。

  1. parent的平衡因子更新之后为2或-2

如果是2或-2呢?要继续往上更新吗?

🆗,如果是2或-2,那已经不在平衡因子的正常范围内了,那就说明当前parent所在的这棵子树已经不平衡了!!!(通常把这棵树叫做最小不平衡子树)
那还往上更新个屁啊,是不是就要去调整结点是这棵最小不平衡子树重变平衡
那怎么调整呢,要进行旋转,具体怎么做后面再讲。
那旋转之后它的高度其实就恢复到插入之前了,也就不需要再继续往上更新了。

  1. parent的平衡因子更新之后为0

那为0的话需要继续更新吗?

为0当然就不需要了。
为什么呢?
大家想,更新之后为0的话,是不是说明插入之前它的平衡因子为1或者-1啊,然后我们在左边插入了一个结点或者是右边,然后它的平衡因子就变成了0
在这里插入图片描述
那他的高度是不是没有发生变化啊,所以不需要继续更新,也不需要调整,插入就结束了。

4.4 平衡因子更新代码实现

我们来写一下代码:

因为不知道要向上更新几次,所以肯定是一个循环。
那循环什么时候结束?
通过我们上面的分析,它可能向上更新几次就停止了,但是不排除有可能一直更新直到根结点
比如这种情况在这里插入图片描述
所以整个循环的结点条件是这样的
在这里插入图片描述
根结点没有父亲(根结点的parent为空),所以如果parent不为空,就有可能要一直向上更新。
然后循环体里面的内容就按照我们上面的分析写就行了
在这里插入图片描述

//更新平衡因子
while (parent)
{
	//更新parent的平衡因子
	if (cur == parent->_right)
	{
		parent->_bf++;
	}
	else
	{
		parent->_bf--;
	}
	//判断是否需要继续向上更新,需要就往上走等待下次循环更新,
	//如果不平衡了就进行处理,不需要处理不需要调整就break
	if (parent->_bf == 1 || parent->_bf == -1)
	{
		parent = parent->_parent;
		cur = cur->_parent;
	}
	else if(parent->_bf == 2 || parent->_bf == -2)
	{
		//进行旋转调整
		//...
		break;
	}
	else if (parent->_bf == 0)
	{
		break;
	}
	else
	{
		//非正常情况
		assert(false);
	}
}

那接下来我们就来重点讲一下对于不平衡的情况如何进行调整,即AVL树的旋转

5. AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。

根据节点插入位置的不同,AVL树的旋转分为四种,接下来我们将一 一进行学习

5.1 新节点插入较高右子树的右侧—右右:左单旋

我们先来学习第一种旋转——左单旋。

什么情况要进行左单旋

那什么样的情况要进行左单旋呢?

在这里插入图片描述
就是上图的这种情况。

大家对照着图,我们来分析一下:

首先大家可能有疑问

在这里插入图片描述
图里面的a、b、c是啥啊?
🆗,我们这里给的是一个抽象图,a、b、c分别代表三棵高度为h的AVL子树,这里的h可以为任何整数值(所以h取不同的值,这里具体的情况是有很多种的,不过不用担心,针对这一类情况,我们的处理是统一的)。

那我们看这个图

原本30这棵AVL树(当然实际中他也可能是一棵子树,子树的话上面就还有结点)处在平衡的状态,右子树比左子树高1,然后现在我们在它的右子树的右侧c这里插入新结点,然后它的高度变成h+1。
注意我们讨论的情况是插入之后它的高度+1,如果高度不变的话也不需要调整了
在这里插入图片描述
还有就是如果插入之后,c的高度虽然+1了,但是c这棵子树直接变的不平衡了
在这里插入图片描述
这两种情况不是我们现在要讨论的。
我们现在讨论的情况就是插入之后c的高度变成h+1了,并且平衡因子需要向上更新影响到30,导致30这棵树不平衡
在这里插入图片描述
比如这样的
在这里插入图片描述
那针对这种情况我们要进行左单旋处理(不论这里的高度h对应是几,这种情况都是左单旋处理)。
大家可以自己多画几个h为不同高度的图。

如何进行左单旋

那左单旋处理是怎么做呢?

现在我们插入之后是这样的
在这里插入图片描述
现在30这个结点的平衡因子是不在正常范围内的,这棵树是不平衡的,右边高,所以要对30这棵树进行左单旋,怎么左单旋呢?
🆗,其实两步就搞定了
在这里插入图片描述
相当于把30往左边向下旋转,所以叫左单旋。
大家看,进行了左单旋之后,这棵树是不是就重新变成AVL树,达到平衡状态了啊,树的高度也降下去了。
为什么这样旋转,大家看60的左子树比60小,比30大,所以可以做30的右子树,然后30整棵树都比60小,所以可以做60的左子树。
当然降高度是一方面,在使它变平衡的同时是不是也要保持它依旧是一颗搜索二叉树啊,因为AVL树就是平衡的搜索二叉树嘛(大家可以看我们旋转过程选择的孩子都是满足搜索树的大小关系的)。
大家可以把h换成实际的数字,画一个图,然后进行一下插入、左单旋,再理解一下这个过程。
在这里插入图片描述
这是抽象图的一个完整过程。

左单旋代码实现

那然后我们来写一下左单旋的代码:

在这里插入图片描述
旋转的时候传要旋转的子树的根结点即可。
然后我们可以把需要操作到的几个结点获取一下
在这里插入图片描述
在这里插入图片描述
然后,按照上面讲的思路进行旋转就行了
在这里插入图片描述
ps:解释一下为什么起名subR,sub是subtree (子树)的缩写
对照着图,大家看一下这样写对不对。

🆗,这样写是有问题的:

第一个问题——没有处理结点的_parent指针
我们上面实现的时候给结点增加了一个指向其父亲的指针_parent,方便我们更新平衡因子的时候往上走,但是代价就是需要我们去维护这个指针。
所以,旋转之后要更新_parent指针
在这里插入图片描述
然后呢,还不行
第二个问题——subRL 可能为空
为什么?
在这里插入图片描述
看图,如果h等于0的话subRL是不是就是空啊。
在这里插入图片描述
所以加个判断。
接着,第三个问题——parent上面可能还有结点(即旋转的是子树)
我们上面分析的时候说了,我们这里旋转的可能是一整棵树,也可能是一棵树中的子树。
所以如果是子树的话,上面还有结点
在这里插入图片描述
这样我们旋转之后上面结点的指向就不对了
所以我们也要处理一下,判断它是不是子树,然后进行不同的处理
在这里插入图片描述
在这里插入图片描述
最后,还有一个问题——旋转之后要更新一下平衡因子
在这里插入图片描述
至此,我们的左单旋才算完成

//左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	if(subRL)
		subRL->_parent = parent;

	//先保存一下parent->_parent,因为下面会改它
	Node* pparent = parent->_parent;

	subR->_left = parent;
	parent->_parent = subR;

	//若pparent为空则证明旋转的是一整棵树,因为根结点的_parent为空
	if (pparent == nullptr)
	{
		//subR是新的根
		_root = subR;
		_root->_parent == nullptr;
	}
	//若pparent不为空,则证明旋转的是子树,parent上面还有结点
	else
	{
		//让pparent指向子树旋转之后新的根
		if (pparent->_left == parent)
		{
			pparent->_left = subR;
		}
		else
		{
			pparent->_right = subR;
		}
		//同时也让新的根指向pparent
		subR->_parent = pparent;
	}
	//旋转完更新平衡因子
	parent->_bf = subR->_bf = 0;
}

什么时候调用左单旋

那我们代码写好了,什么时候调用呢?

我们观察图会发现
在这里插入图片描述
如果parent的平衡因子是2,subR(对应我们在更新平衡因子的那个循环里就是cur)的平衡因子是1,此时要进行的就是左单旋
在这里插入图片描述

if (parent->_bf == 2 && cur->_bf == 1)
{
	RotateL(parent);
}

5.2 新节点插入较高左子树的左侧—左左:右单旋

接着我们看第二种旋转——右单旋

什么情况要进行右单旋

那右单旋又适用于哪些情况呢呢?

在这里插入图片描述
同样的我们这里讨论的情况是插入之后a的高度要发生变化,且会影响到当前这棵树(当然它可以是一棵子树)根结点的平衡因子,导致整棵树不平衡,这时我们可以用右单旋解决。

如何进行右单旋

那右单旋又该如何操作呢?

也是两步
在这里插入图片描述
相当于把30往右边向上旋转,所以叫右单旋。
30的右子树比30大,比60小,所以可以做60的左子树,然后60整棵树都比30大,所以可以做30的右子树。
这样这棵树就重新变平衡了,30成为了新的根结点。

右单旋代码实现

那我们来写一下右单旋的代码

那写了上面左单旋的代码,再写右单旋的话应该就比较轻松了,需要注意的点还是那几个
在这里插入图片描述
对照着图,我们来写一下,这里我就不做过多解释了
在这里插入图片描述

//右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = parent->_right;

	//旋转并更新_parent指针
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;
	
	//先保存一下parent->_parent,因为下面会改它
	Node* pparent = parent->_parent;

	//旋转并更新_parent指针
	subL->_right = parent;
	parent->_parent = subL;

	//若parent等于_root则证明旋转的是一整棵树(这也是一种判断方法)
	if (parent == _root)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		//让pparent指向子树旋转之后新的根
		if (parent == pparent->_left)
		{
			pparent->_left = subL;
		}
		else
		{
			pparent->_right = subL;
		}
		//同时也让新的根指向pparent
		subL->_parent = pparent;
	}
	subL->_bf = parent->_bf = 0;
}

什么时候调用右单旋

那右单旋什么时候调用呢?

来看图
在这里插入图片描述
🆗,我们看到如果parent的平衡因子为-2,subL(cur)的平衡因子为-1,要调用的就是右单旋
在这里插入图片描述

5.3 新节点插入较高左子树的右侧—左右:先左单旋再右单旋(左右双旋)

再来看第三种旋转——左右双旋

什么情况进行左右双旋

看这张图
在这里插入图片描述
这里给的是在b插入,在c插入当然也是左右双旋,但是插入之后平衡因子的更新会有一些不同,后面会提到。
这还是抽象图,我们来画几个具象图看一下
在这里插入图片描述

如何进行左右双旋

首先要知道对于这种情况,我们如果只进行左或者右的单旋是解决不了问题的

在这里插入图片描述
大家看这种情况插入之后根结点90是-2,-2就表明左边高嘛。
那左边高的话如果我们进行右旋可以变平衡吗?
那对它右旋之后是这样的
在这里插入图片描述
这是不是还不平衡啊,现在变成右边高了

那要进行双旋,怎么做呢?

上面已经说了针对这种情况要进行的是左右双旋,那顾名思义就是先进行一个左单旋(对根的左子树),再进行一个右单旋(对根)
在这里插入图片描述
然后就平衡了,其实我们能发现它就是把60推上去做根,然后60的左右子树分给30的右子树和90的左子树。
为什么不能直接右单旋,因为大家看他原来不是一个单纯的左边高
插入之后类似这样一个形状
在这里插入图片描述
首先第一步的左单旋相当于把它变成单纯的左边高
在这里插入图片描述
然后在进行一次右单旋,就平衡了。

左右双旋代码实现

那左右双旋的代码怎么写?是不是直接复用左右单旋的代码就行了

那就先调用左旋,再调用右旋就行了
在这里插入图片描述
但是左右双旋麻烦的地方其实在于平衡因子的调节。
我们上面提到插入在b和c它们最后平衡因子更新不同
在这里插入图片描述
能看到旋转之后它们的平衡因子更新是不一样的。
那如何判断在b插入还是在c插入呢?
🆗,大家看图,不同位置的插入,插入之后60这个结点的平衡因子是不同的。
那除此之外,h为0的时候,其实平衡因子的更新又有所不同
如果h==0的话
在这里插入图片描述
它旋转是这样的
在这里插入图片描述
所以,平衡因子的更新这里我们要分三种情况
我们还是记录一下这三个结点,方便操作
在这里插入图片描述
然后我们补充一下平衡因子更新的代码,不同情况更新不同的值
在这里插入图片描述

//左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;

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

	//更新平衡因子
	if (bf == -1)
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1)
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	else if (bf == 0)
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

什么时候调用左右双旋

看图

在这里插入图片描述
在这里插入图片描述

else if (parent->_bf == -2 && cur->_bf == 1)
{
	RotateLR(parent);
}

5.4 新节点插入较高右子树的左侧—右左:先右单旋再左单旋(右左双旋)

什么情况进行右左双旋

那我们来看一下右左双旋适用于哪些情况?

在这里插入图片描述
当然插入到b这棵树上也是可以的。
同样的高度h不同,就会产生很多不同的情况,但是没关系,这要是这种情况,我们就可以统一处理

如何进行右左双旋

那就还是两次单旋嘛:

这里就是首先进行一次右单旋(对根的右子树),然后再进行一次左单旋(对根)
在这里插入图片描述
最后就平衡了。
其实根上面学的左右双旋是同样的道理:
这样的情况只旋一次是不能达到平衡的,所以第一次其实是把它变成纯粹的右边高,然后再进行一次左单旋就平衡了。
那最终的结果就相当于把60推上去做根,然后60的左右子树分别分给30的右子树和90的左子树。
在这里插入图片描述

右左双旋代码实现

那右左单旋的话我们可以是可以直接复用左单旋和右单旋的,但是,同样的道理,我们还是需要对双旋之后的平衡因子分不同的情况进行更新处理:

与左右双旋一样,还是三种情况,不同情况平衡因子的更新不同,通过插入之后subRL的平衡因子区分三种情况

  1. 就是我们上面分析的,在c插入
    在这里插入图片描述
  2. 在b插入
    在这里插入图片描述
  3. h等于0情况下的插入
    在这里插入图片描述

那对应的代码就是
在这里插入图片描述

//右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

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

	//更新平衡因子
	if (bf == 1)
	{
		parent->_bf = -1;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else if (bf == 0)
	{
		parent->_bf = 0;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

什么时候调用右左双旋

很容易看出来:

在这里插入图片描述
当根结点的平衡因子为2,cur为-1的时候调用的是右左双旋
在这里插入图片描述

5.5 总结

假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑

  1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为SubR
    当SubR的平衡因子为1时,执行左单旋
    当SubR的平衡因子为-1时,执行右左双旋
  2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为SubL
    当SubL的平衡因子为-1是,执行右单旋
    当SubL的平衡因子为1时,执行左右双旋
    在这里插入图片描述
    旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

6. AVL树的测试

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要测试AVL树,可以分两步:

6.1 验证其为二叉搜索树

我们插入一些数据,如果中序遍历可得到一个有序的序列,就说明为二叉搜索树

在这里插入图片描述
我们定义一棵AVL树,然后插入一些数据中序遍历一下。
写一个中序遍历
在这里插入图片描述
在这里插入图片描述
然后我们运行一下
在这里插入图片描述
🆗,没什么问题,是有序的。

6.2 验证其为平衡树

那如何验证它是否平衡呢?

我们可以去计算高度,如果每一个结点左右子树的高度差的绝对值不超过1,就证明它是平衡的。
为什么不用平衡因子判断呢?
首先,不是所有的AVL树的实现里面都有平衡因子的,只是我们这里采用了平衡因子,这是AVL树的一种实现方法而已。
其次,我们不敢保证我们自己写到代码计算出来的平衡因子一定是正确的。

所以,我们来写一个通过高度差来判断是否平衡的函数

在这里插入图片描述
这个比较简单,我就不过多解释了

然后我们测试一下

先判断一下刚才的那棵树在这里插入图片描述
🆗,是平衡的。
我们再来看一个比较特殊的场景
{4, 2, 6, 1, 3, 5, 15, 7, 16, 14}
在这里插入图片描述
这个是一个右左双旋的场景
在这里插入图片描述
没什么问题。
如果我们不调整的话
在这里插入图片描述
那它应该就是不平衡了
在这里插入图片描述
没问题。

然后呢,我们还可以做一件事情

6.3 判断平衡因子的更新是否正确

怎么判断:

很简单,计算一下高度差,看他和平衡因子相不相等就行了
在这里插入图片描述
再来测试一下
在这里插入图片描述
没有问题,还是平衡。

6.4 大量随机数构建AVL树进行测试

上面的测试数据量比较小,且不够随机

下面我们生成一些随机数来构建AVL树,测试一下

在这里插入图片描述
10万个随机数,先来试一下
在这里插入图片描述
没有问题,10万个随机数构建也没有出现错误的情况,依然是平衡的。
来,100万个随机数
在这里插入图片描述
依旧没问题。

7. 查找

然后AVL树的查找那就跟搜索二叉树是一样的,我们这里就不讲了,大家可以看之前搜索二叉树的文章。

8. AVL树的删除(了解)

AVL树的删除操作我们不做重点讲解,大家了解一下即可,因为这个不是特别重要,面试一般也不会考到。

AVL树的删除操作主要分为以下几个步骤:

  1. 执行二叉搜索树的删除操作
  2. 更新平衡因子:如果删除之后影响到了上面结点的平衡因子,就要从被删除节点的父节点向上更新受影响的平衡因子。
  3. 检查所有的平衡因子,如果存在不正常的平衡因子,则要对相应的树进行调整,使它恢复平衡。
  4. 重复步骤2和步骤3,直至到达根节点或不需要进一步调整为止。

9. AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即 l o g 2 ( N ) log_2 (N) log2(N)
但是如果要对AVL树做一些结构修改的操作,性能非常低下。
比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

10. 源码

10.1 AVLTree.h

#pragma once
#include <assert.h>

template <class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K,V>* _right;
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	int _bf;//balance factor

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

template <class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			//cout << "root:"<<_root->_kv.first << endl;
			return true;
		}
		
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		//链接父亲指针
		cur->_parent = parent;

		//更新平衡因子
		while (parent)
		{
			//更新parent的平衡因子
			if (cur == parent->_right)
			{
				parent->_bf++;
			}
			else
			{
				parent->_bf--;
			}
			//判断是否需要继续向上更新,需要就往上走等待下次循环更新,
			//如果不平衡了就进行处理,不需要处理不需要调整就break
			if (parent->_bf == 1 || parent->_bf == -1)
			{
				parent = parent->_parent;
				cur = cur->_parent;
			}
			else if(parent->_bf == 2 || parent->_bf == -2)
			{
				//根据实际情况进行相应的旋转调整
				if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				else
				{
					assert(false);
				}
				//旋转完结束,就不需要再往上更新了
				break;
			}
			else if (parent->_bf == 0)
			{
				break;
			}
			else
			{
				//非正常情况
				assert(false);
			}
		}
		//cout << "root:" << _root->_kv.first << endl;
		return true;
	}
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
	bool IsBalance()
	{
		return _IsBalance(_root);
	}
	int TreeHeight()
	{
		return _TreeHeight(_root);
	}
private:
	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;
		int rightH = _TreeHeight(root->_right);
		int leftH = _TreeHeight(root->_left);

		if (rightH - leftH != root->_bf)
		{
			cout << root->_kv.first << "结点平衡因子更新错误" << endl;
			return false;
		}

		return abs(rightH - leftH) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}
	int _TreeHeight(Node* root)
	{
		if (root == nullptr)
			return 0;
		int RightH = _TreeHeight(root->_left);
		int leftH = _TreeHeight(root->_right);
		return RightH > leftH ? RightH + 1 : leftH + 1;
	}
	//左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		//旋转并更新_parent指针
		parent->_right = subRL;
		if(subRL)
			subRL->_parent = parent;

		//先保存一下parent->_parent,因为下面会改它
		Node* pparent = parent->_parent;

		//旋转并更新_parent指针
		subR->_left = parent;
		parent->_parent = subR;

		//若pparent为空则证明旋转的是一整棵树,因为根结点的_parent为空
		if (pparent == nullptr)
		{
			//subR是新的根
			_root = subR;
			_root->_parent = nullptr;
		}
		//若pparent不为空,则证明旋转的是子树,parent上面还有结点
		else
		{
			//让pparent指向子树旋转之后新的根
			if (pparent->_left == parent)
			{
				pparent->_left = subR;
			}
			else
			{
				pparent->_right = subR;
			}
			//同时也让新的根指向pparent
			subR->_parent = pparent;
		}
		//旋转完更新平衡因子
		parent->_bf = subR->_bf = 0;
	}

	//右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		//旋转并更新_parent指针
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		
		//先保存一下parent->_parent,因为下面会改它
		Node* pparent = parent->_parent;

		//旋转并更新_parent指针
		subL->_right = parent;
		parent->_parent = subL;

		//若parent等于_root则证明旋转的是一整棵树(这也是一种判断方法)
		if (parent == _root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			//让pparent指向子树旋转之后新的根
			if (parent == pparent->_left)
			{
				pparent->_left = subL;
			}
			else
			{
				pparent->_right = subL;
			}
			//同时也让新的根指向pparent
			subL->_parent = pparent;
		}
		subL->_bf = parent->_bf = 0;
	}
	//左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

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

		//更新平衡因子
		if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_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);

		//更新平衡因子
		if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

10.2 Test.cpp

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


void AVLTest1()
{
	//int arr[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	//int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	//int arr[] = { 1,2,3,4,5,6,7,8,9,1 };
	int arr[] = { 95,47,32,29,7,7,2,50,74,30 };

	AVLTree<int, int> t1;
	for (auto e : arr)
	{
		//cout << e << endl;
		t1.Insert(make_pair(e, e));
		t1.InOrder();
		if (!t1.IsBalance())
		{
			break;
		}
	}
	//t1.InOrder();
	/*if (t1.IsBalance())
	{
		cout << "平衡" << endl;
	}
	else
	{
		cout << "不平衡" << endl;
	}*/
}

void AVLTest2()
{
	srand(time(nullptr));
	const int N = 1000000;
	AVLTree<int, int> t;
	for (int i = 0; i < N; ++i)
	{
		int x = rand();
		t.Insert(make_pair(x, x));
	}
	if (t.IsBalance())
	{
		cout << "平衡" << endl;
	}
	else
	{
		cout << "不平衡" << endl;
	}
}

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

在这里插入图片描述

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

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

相关文章

【最全Kubernetes容器编排安装教程】

文章目录 环境准备主机间做信任使用ssh-copy-id命令实现免密登录的安装与配置1. 安装Git Bash软件2. 生成公钥3. SSH登录Linux服务器4. 公钥复制到Linux服务器5. SSH免密登录测试 安装ansible工具 升级内核版本使用elrepo源升级内核查看最新版内核安装最新的内核版本设置系统默…

C语言的链表的相关操作

本变博客源于自己想复习一下C语言&#xff0c;所以便自己动手复习了一下链表的相关操作。做个人记录使用。 main.c #include <stdio.h> #include "list.h"int main() {student *a;printf("hello world\n") ;printf("----初始化列表----------\…

阻塞队列的安全实现,定时器的安全实现(面试易考),超详细

一、&#x1f49b; 如何实现一个线程安全的阻塞队列 目前&#xff0c;当前代码是循环队列&#xff08;还没有进行改动&#xff09; head和tail的判空判断满两种方法: 1.浪费一个格子&#xff0c;当前走到head的前一个位置&#xff0c;就认为队列满的 2.单独搞一个变量&#xff…

【分布式系统】聊聊分布式事务中原子性

什么是分布式事务 在分布式系统中&#xff0c;一个是计算问题&#xff0c;也就是将多个任务&#xff0c;通过流控技术把不同的流量分发给不同的服务器进行处理。另一个就是存储&#xff0c;而只要设计的存储&#xff0c;就必然会引入从单体事务中衍生除的分布式事务问题。 事务…

css flex 上下结构布局

display: flex; flex-flow: column; justify-content: space-between;

战略方法论

父文章 人人都是战略家 2018年注册会计师公司战略与风险考点:swot分析_知识点_注册会计师 SWOT分析 一、基本原理 所谓SWOT分析&#xff0c;即基于内外部竞争环境和竞争条件下的态势分析&#xff0c;就是将与研究对象密切相关的各种主要内部优势、劣势和外部的机会和威胁等…

提高WordPress网站性能的24个技巧

你想加速你的WordPress网站吗&#xff1f;快速加载页面可改善用户体验、增加页面浏览量并帮助你优化WordPress SEO。在本文中&#xff0c;我们将分享最有用的WordPress网站性能速度优化技巧&#xff0c;以提高WordPress网站性能并加快你的网站速度。 与其他“X 优秀的 WordPres…

redis学习笔记(九)

文章目录 python对redis基本操作&#xff08;1&#xff09;连接redis&#xff08;2&#xff09;数据类型操作 python对redis基本操作 &#xff08;1&#xff09;连接redis # 方式1 import redisr redis.Redis(host127.0.0.1, port6379) r.set(foo, Bar) print(r.get(foo))# …

Xilinx DDR3学习总结——1、MIG核设置

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 Xilinx DDR3学习总结——1、MIG核设置 前言开发板 DDR信息MIG 设置前言 话说之前从来没有使用过DDR,工作中的项目都是流式处理的,没有一个大存储的需求,应该图像处理中DDR用的会比较多一些,但是一个这么…

SpringBoot05--axios网络请求

浏览器主动发送请求&#xff0c;服务器接收请求之后返回数据&#xff0c;通过vue进行数据绑定 请求成功&#xff0c;返回的数据会包装到response里面去&#xff08;返回成response的data属性&#xff09; 好的这边不太懂 在xxx.vue组件被创建和挂载&#xff08;渲染&#xff09…

在idea运行python文件

在idea运行python文件 如果在idea运行python文件而没有弹出run的选项&#xff0c;则点击File->Settings…->Plugins&#xff0c;在里面搜索python&#xff0c;如果没有显示则在Maketplace进行搜索&#xff0c; 接着Install&#xff0c;然后restart

C++ STL list

✅<1>主页&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;C之 STL list介绍和模拟实现 ☂️<3>开发环境&#xff1a;Visual Studio 2022 &#x1f4ac;<4>前言&#xff1a;上次我们详细的介绍了vector&#xff0c;今天我们继续来介绍…

某大厂笔试(小*的车站的最近距离)

有一个环形的公路&#xff0c;上面共有n站&#xff0c;现在给定了顺时针第i站到第i1站之间的距离&#xff08;特殊的&#xff0c;也给出了第n站到第1站的距离&#xff09;&#xff0c;小*想着沿着公路第x站走到第y站&#xff0c;她想知道最短的距离是多少&#xff1f; 输入描述…

无涯教程-Perl - print函数

描述 此函数将LIST中的表达式的值打印到当前的默认输出文件句柄或FILEHANDLE指定的句柄中。 如果设置,则$\变量将添加到LIST的末尾。 如果LIST为空,则打印$_中的值。 print接受一个值列表,列表中的每个元素都将被解释为一个表达式。 语法 以下是此函数的简单语法- print…

谷歌发布多平台应用开发神器:背靠 AI 编程神器 Codey,支持 React、Vue 等框架,还能代码补全

一、概述 8 月 8 日&#xff0c;谷歌宣布推出 AI 代码编辑器 IDX&#xff0c;旨在提供基于浏览器的人工智能开发环境&#xff0c;用于构建全栈网络和多平台应用程序。谷歌在创建 IDX 时并没有构建新的 IDE&#xff08;集成开发环境&#xff09;&#xff0c;而是使用 VS Code 作…

网络安全(黑客)自学路线/笔记

想自学网络安全&#xff08;黑客技术&#xff09;首先你得了解什么是网络安全&#xff01;什么是黑客&#xff01; 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全…

代码随想录算法训练营第55天|动态规划part12|309.最佳买卖股票时机含冷冻期、714.买卖股票的最佳时机含手续费、总结

代码随想录算法训练营第55天&#xff5c;动态规划part12&#xff5c;309.最佳买卖股票时机含冷冻期、714.买卖股票的最佳时机含手续费、总结 309.最佳买卖股票时机含冷冻期 309.最佳买卖股票时机含冷冻期 思路&#xff1a; 区别在第i天持有股票的当天买入的情况&#xff0c…

【Kubernetes】神乎其技的K8s到底是什么,为什么被越来越多人使用

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;陈童学哦&#xff0c;目前学习C/C、算法、Python、Java等方向&#xff0c;一个正在慢慢前行的普通人。 &#x1f3c0;系列专栏&#xff1a;陈童学的日记 &#x1f4a1;其他专栏&#xff1a;CSTL&…

户外骨传导耳机推荐,盘点最适合户外佩戴的五款耳机

现在天气越来越暖和了&#xff0c;很多人选择外出徒步、越野或者骑行&#xff0c;在这些活动中往往都会搭配一个骨传导耳机&#xff0c;来让运动过程变得更加有趣。在选购骨传导耳机时&#xff0c;人们通常会考虑音质、舒适性、价格等因素&#xff0c;为了让大家选到更适合自己…

Kafka API与SpringBoot调用

文章目录 首先需要命令行创建一个名为cities的主题&#xff0c;并且创建该主题的订阅者。 1、使用Kafka原生API1.1、创建spring工程1.2、创建发布者1.3、对生产者的优化1.4、批量发送消息1.5、创建消费者组1.6 消费者同步手动提交1.7、消费者异步手动提交1.8、消费者同异步手动…