AVL树介绍

AVL树是一种自平衡的二叉搜索树,通过插入操作后进行旋转保持树的平衡,以确保高效的查找性能。插入时,先按二叉搜索树规则找到位置,然后更新平衡因子,若不平衡则进行左单旋、右单旋、左右双旋或右左双旋四种旋转之一。平衡因子的更新和旋转处理确保了树的高度保持在O(log2N),维持了良好的查找效率。
摘要由CSDN通过智能技术生成

AVL树的概念

二叉搜索树虽然可以提高我们查找数据的效率,但如果插入二叉搜索树的数据是有序或接近有序的,此时二叉搜索树会退化为单支树,在单支树当中查找数据相当于在单链表当中查找数据,效率是很低下的

因此,两位俄罗斯的数学家G.M.A delson-Velskii和E.M.Landis在1962年发明了解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

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

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

如果一棵二叉搜索树的高度是平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(log2 N),搜索时间复杂度也是O(log2 N)。

只有满二叉树才能做到每个结点左右子树高度之差均为0

ps:AVL树不一定需要平衡因子,平衡因子只是一种控制实现方式。

AVL树结点的定义

我们这里直接实现K V模型的AVL树,为了方便后续的操作,这里将AVL树中的结点定义为三叉链结构,并在每个结点当中引入平衡因子(右子树高度-左子树高度)。除此之外,还需编写一个构造新结点的构造函数,由于新构造结点的左右子树均为空树,于是将新构造结点的平衡因子初始设置为0即可。

template<class K, class V>
struct AVLTreeNode
{
	//三叉链
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	//存储的键值对
	pair<K, V> _kv;

	//平衡因子(balance factor)
	int _bf; //右子树高度-左子树高度

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

AVL树的插入

AVL树插入结点时有以下三个步骤:

  • 按照二叉搜索树的插入方法,找到待插入位置。
  • 找到待插入位置后,将待插入结点插入到树中。
  • 更新平衡因子,如果出现不平衡,则需要进行旋转。

因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:

  • 待插入结点的key值比当前结点小就插入到该结点的左子树。
  • 待插入结点的key值比当前结点大就插入到该结点的右子树。
  • 待插入结点的key值与当前结点的key值相等就插入失败。

如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。


AVL树插入结点后需要更新树中结点的平衡因子,由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。

所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:

  • 新增结点在parent的右边,parent的平衡因子+ + 。
  • 新增结点在parent的左边,parent的平衡因子− −。

每更新完一个结点的平衡因子后,都需要进行以下判断:

  • 如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
  • 如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
  • 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

注意:

  • parent的平衡因子在更新前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。
  • 在最坏情况下,我们更新平衡因子时会一路更新到根结点。

若是在更新平衡因子的过程当中,出现了平衡因子为-2/2的结点,这时我们需要对以该结点为根结点的树进行旋转处理,而旋转处理分为四种,在进行分类之前我们首先需要进行以下分析:

当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

理由如下:

若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新。
而cur是新增结点的话,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,因为新增结点最终会插入到一个空树当中,在新增结点插入前,其父结点的状态有以下两种可能:

  • 其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。
  • 其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。

综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

根据此结论,我们可以将旋转处理分为以下四类:

  • 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。
  • 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
  • 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。
  • 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。

并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了。

插入代码如下:

//插入
	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 (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else//待插入结点的key值等于当前结点的key值
			{
				return false;//插入失败(不允许key值冗余)
			}
		}
		//找到插入位置
		cur = new Node(kv);
		if (parent->_kv.first < cur->_kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;//三叉链结构,保证可以双向找到
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		// 控制树的平衡
		//1.更新平衡因子--》新增节点到根节点的祖先路径
		//2.出现异常平衡因子,则需要旋转平衡处理。
		while (parent)
		{
			// 0、更新平衡因子
			if (cur == parent->_left)//插入位置是parent左边
				parent->_bf--;
			else
				parent->_bf++;

			// 检查父亲的平衡因子

			// 1、父亲所在子树的高度不变,不影响祖先,更新结束
			if (parent->_bf == 0)
			{
				break;
			} // 2、父亲所在子树的高度变了,继续往上更新
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				cur = parent;
				parent = parent->_parent;
			} // 3、父亲所在子树的出现了不平衡,需要旋转处理
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				if (parent->_bf == -2 && cur->_bf == -1)
				{
					// 右单旋
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == 1)
				{
					// 左单旋
					RotateL(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
			{
				assert(false);//说明在插入时这棵树就已经出问题了
			}
		}

		return true;
	}

AVL树的旋转

(1)左单旋

左单旋的步骤如下:

  • 让subR的左子树作为parent的右子树。
  • 让parent作为subR的左子树。
  • 让subR作为整个子树的根。
  • 更新平衡因子。

举例:

当在子树c上插入一个新节点.此时子树c的高度变化为h+1,子树b的高度为h.结点5的平衡因子变为1,结点3的平衡因子变为2.此树不再平衡

左单旋:整体向左方向旋转。

  • 结点5的左子树b变为结点3的右子树
  • 结点3以及其子树变为结点5的左子树

在这里插入图片描述

左单旋后满足二叉搜索树的性质:

  • subR的左子树当中结点的值本身就比parent的值大,因此可以作为parent的右子树。
  • parent及其左子树当中结点的值本身就比subR的值小,因此可以作为subR的左子树。

可以看到,经过左单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左单旋后无需继续往上更新平衡因子。

注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。

代码如下:

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

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

		Node* ppNode = parent->_parent;

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

		if (parent == _root)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}

			subR->_parent = ppNode;
		}

		subR->_bf = parent->_bf = 0; //更新平衡因子
	}
(2)右单旋

右单旋的步骤如下:

  • 让subL的右子树作为parent的左子树。
  • 让parent作为subL的右子树。
  • 让subL作为整个子树的根。
  • 更新平衡因子。

举例:

当在子树a的下面插入一个结点时,结点3的左子树高度变为h+1,右子树高度为h,平衡因子变为-1.结点5的左子树高度变为h+2,右子树高度变为h,平衡因子变为-2.此时不再平衡。

右单旋:整体向右方向旋转。

  • 结点5变为结点3的右子树
  • 结点3右子树以及其子树变为结点5的左子树
  • parent指向结点3

在这里插入图片描述

右单旋后满足二叉搜索树的性质:

  • subL的右子树当中结点的值本身就比parent的值小,因此可以作为parent的左子树。
  • parent及其右子树当中结点的值本身就比subL的值大,因此可以作为subL的右子树。

代码如下:

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

		parent->_left = subLR;
		if (subLR)//当子树高度为0,subLR为空
			subLR->_parent = parent;

		Node* ppNode = parent->_parent;//防止parent也是一棵树的子树,parent也有自己的parent,用ppNode记录parent的parent

		subL->_right = parent;
		parent->_parent = subL;

		if (parent == _root)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else//说明parent也是一棵树的子树,parent也有自己的parent
		{
			//此时将subL作为当前子树的根,将原先parent的parent指向当前的subL
			if (ppNode->_left == parent)
				ppNode->_left = subL;
			else
				ppNode->_right = subL;

			subL->_parent = ppNode;
		}

		parent->_bf = subL->_bf = 0;
	}
(3)左右双旋

新结点插入到子树b和c会有不同的情况,首先以在子树b下插入一个结点为例,此时结点4的平衡因子变为-1,结点3的平衡因子变为1,结点5的平衡因子变为-2. AVL树不再平衡。

左右双旋:先左单旋,再右单旋。不过两次的对象不同。先对结点3和结点4进行左单旋,然后进行右单旋。
在这里插入图片描述


在这里插入图片描述
以上图为例,左右双旋的步骤如下:

  • 以subL为旋转点进行左单旋。
  • 以parent为旋转点进行右单旋。
  • 更新平衡因子。

左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:

1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。
在这里插入图片描述

2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。
在这里插入图片描述

3、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。
在这里插入图片描述
可以看到,经过左右双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左右双旋后无需继续往上更新平衡因子。

代码如下:

//左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf; //subLR不可能为nullptr,因为subL的平衡因子是1

	//1、以subL为旋转点进行左单旋
	RotateL(subL);

	//2、以parent为旋转点进行右单旋
	RotateR(parent);

	//3、更新平衡因子
	if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false); //在旋转前树的平衡因子就有问题
	}
}
(4)右左双旋

同样也是有三种情况,这里我以新节点插入到c子树为例分析:
在这里插入图片描述
调整过程:

在这里插入图片描述


在这里插入图片描述
以上图为例,右左双旋的步骤如下:

  • 以subR为旋转点进行右单旋。
  • 以parent为旋转点进行左单旋。
  • 更新平衡因子。

右左双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:

1、当subRL原始平衡因子是1时,左右双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。
在这里插入图片描述

2、当subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。
在这里插入图片描述

3、当subRL原始平衡因子是0时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、0、0。
在这里插入图片描述
代码如下:

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

	//1、以subR为轴进行右单旋
	RotateR(subR);

	//2、以parent为轴进行左单旋
	RotateL(parent);

	//3、更新平衡因子
	if (bf == 1)
	{
		subRL->_bf = 0;
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == -1)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 0)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false); //在旋转前树的平衡因子就有问题
	}
}

AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,也就是说AVL树也是二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断二叉树是否为二叉搜索树。

//中序遍历
void Inorder()
{
	_Inorder(_root);
}
//中序遍历子函数
void _Inorder(Node* root)
{
	if (root == nullptr)
		return;
	_Inorder(root->_left);
	cout << root->_kv.first << " ";
	_Inorder(root->_right);
}

但中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。

采用后序遍历,变量步骤如下:

  • 从叶子结点处开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)
  • 先判断左子树是否是平衡二叉树。
  • 再判断右子树是否是平衡二叉树。
  • 若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)
    在这里插入图片描述

代码:

//判断是否为AVL树
bool IsAVLTree()
{
	int hight = 0; //输出型参数
	return _IsBalanced(_root, hight);
}
//检测二叉树是否平衡
bool _IsBalanced(Node* root, int& hight)
{
	if (root == nullptr) //空树是平衡二叉树
	{
		hight = 0; //空树的高度为0
		return true;
	}
	//先判断左子树
	int leftHight = 0;
	if (_IsBalanced(root->_left, leftHight) == false)
		return false;
	//再判断右子树
	int rightHight = 0;
	if (_IsBalanced(root->_right, rightHight) == false)
		return false;
	//检查该结点的平衡因子
	if (rightHight - leftHight != root->_bf)
	{
		cout << "平衡因子设置异常:" << root->_kv.first << endl;
	}
	//把左右子树的高度中的较大值+1作为当前树的高度返回给上一层
	hight = max(leftHight, rightHight) + 1;
	return abs(rightHight - leftHight) < 2; //平衡二叉树的条件
}

AVL树的性能

  • AVL树是一棵绝对平衡的二叉搜索树,其要求每个结点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即l o g N 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。

因此,如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但当一个结构经常需要被修改时,AVL树就不太适合了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

…狂奔的蜗牛~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值