AVL树【图解】

AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均 搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
1.它的左右子树都是avl树
2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
在这里插入图片描述
注:如果一棵二叉树是平衡二叉树,它就是AVL树,如果有N个结点,O(log_2 n),搜索时间复杂度O(log_2 n)。

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;//balance factor平衡因子
       
       AVLTreeNode(const pair<K, V>& kv)构造函数
        :_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
        ,_kv(kv)
        ,_bf(0)
    {
    }
};


AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
1.按照二叉搜索树的方式插入新节点
2.调整结点的平衡因子
3.如果出现不平衡,就旋转调整

对于二叉搜索树的插入规则:
1.插入结点key比当前节点小就插入左子树
2.插入结点key比当前节点大就插入右子树
3.插入节点key和当前节点相等就插入失败

对于平衡因子的变化:
一个节点的平衡因子是否需要更新,取决于该节点的左右子树的高度是否发生了变化,因此插入一个结点后,该节点的祖先的平衡因子也可能发生变化。
在这里插入图片描述
所以我们要倒序更新,更新规则:
1.如果新增结点在parent的左边,parent的平衡因子++。
2.如果新增结点在parent的右边,parent的平衡因子–。

每次更新完一个平衡因子,都需要继续向上更新 判断:
1.如果parent的平衡因子变为-1 或 1,则需要往上继续更新平衡因子。

解析:就像上面那个图一样,parent变为 1 或者 -1说明它已经不平衡了, parent的parent,因此需要继续向上调整。

2.如果parent平衡因子变为了0,则不需要继续更新

解析:说明新节点插入后,新节点的parent的左右子树平衡了,并不会影响parent的parent,所以不需要继续向上更新

3.如果parent的平衡因子变为了 2 或 -2 ,表明parent结点为 跟结点的子树需要进行旋转调整。

解析:此时已经很不平衡了,正常的调整无法平衡,需要旋转调整

对于旋转调整
我们将插入节点称为cur,它的父节点为parent,当parent的平衡因子变为2/-2 的时候,cur的平衡因子必然为1/-1 ,不可能是0;
解释:假如cur的平衡因子为0,cur已经平衡了, 这时候无论cur在parent的左子树还是右子树,另一个子树无论是否为空,都不能使parent的平衡因子变为2/-2,所以上面成立。
根据这个结论,我们可以将旋转处理分为以下四类:
1.parent的平衡因子为2,cur为1,左单旋转
2.parent的平衡因子为-2,cur为-1,右单旋转
3.parent的平衡因子为-2,cur为1,左右双旋转
4.parent的平衡因子为2,cur为-1,右左双旋转

旋转后就不需要再向上更新平衡因子了。

1.左单旋

在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/6cd326636fd44025b210223be458d970.png在这里插入图片描述

左单旋步骤

  1. subRL变为parent的右子树
  2. parent变为subR的左子树
  3. subR变为这个子树的跟
  4. 更新平衡因子

左单旋并不会破环原本二叉搜索树的性质
b比60大 ,到30的右子树满足比30大。
30比60小 ,到60的左子树满足性质。

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

	//记录pparent
	Node* parentParent = parent->_parent;
	//1、建立subR和parent之间的关系
	parent->_parent = subR;
	subR->_left = parent;

	//2、建立parent和subRL之间的关系
	parent->_right = subRL;
	if (subRL)//判空,防止h为空时候 报错
		subRL->_parent = parent;

	//3、建立parentParent和subR之间的关系
	if (parentParent == nullptr)
	{
		_root = subR;
		subR->_parent = nullptr; //subR的_parent指向需改变
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subR;
		}
		else //parent == parentParent->_right
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}

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

2.右单旋

在这里插入图片描述
右单旋步骤

  1. subLR变为parent的左子树
  2. parent变为subL的右子树
  3. subL变为这个子树的跟
  4. 更新平衡因子

右单旋并不会破环原本二叉搜索树的性质
参考左旋转理解

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

	//记录pparent
	Node* parentParent = parent->_parent;
	//1、建立subL和parent之间的关系
	subL->_right = parent;
	parent->_parent = subL;
	

	//2、建立parent和subLR之间的关系
	parent->_left = subLR;
	if (subLR)//判空防止h为0的时候报错
		subLR->_parent = parent;

	//3、建立parentParent和subL之间的关系
	if (parentParent == nullptr)//判断parent在旋转前是不是根节点
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subL;
		}
		else //parent == parentParent->_right
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}

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

3.左右双旋转:先左旋转再右旋转

在这里插入图片描述

左右双旋的步骤如下:

1.以subL为旋转点进行左单旋。
2.以parent为旋转点进行右单旋。
3.更新平衡因子。

左右双旋后满足二叉搜索树的性质:
就是左旋后右旋,上面解释了左旋和右旋都不影响性质,这只是连起来加了个先后顺序。

左右双旋后,平衡因子的更新随着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;
	//提前记录subLR的平衡因子
	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.右左双旋转:先右旋转再左旋转

在这里插入图片描述

右左双旋的步骤如下:

1.以subR为旋转点进行右单旋。
2.以parent为旋转点进行左单旋。
3.更新平衡因子。

右左双旋后,平衡因子的更新随着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;
	//提前记录subLR的平衡因子
	int bf = subLR->_bf; //subLR不可能为nullptr,因为subL的平衡因子是1

	//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); //在旋转前树的平衡因子就有问题
	}
}

经过旋转后,不平衡的树变得平衡了,而上面的树本来就是平衡的,所以不需要再向上更新平衡因子。

总的插入:

//插入函数
bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr) //若为空树,则插入结点直接作为根结点
	{
		_root = new Node(kv);
		return true;
	}
	//1、按照二叉搜索树的插入方法,找到待插入位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
		{
			//往该结点的左子树走
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
		{
			//往该结点的右子树走
			parent = cur;
			cur = cur->_right;
		}
		else //待插入结点的key值等于当前结点的key值
		{
			//插入失败(不允许key值冗余)
			return false;
		}
	}

	//2、将待插入结点插入到树中
	cur = new Node(kv); //根据所给值构造一个新结点
	if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值
	{
		//插入到parent的左边
		parent->_left = cur;
		cur->_parent = parent;
	}
	else //新结点的key值大于parent的key值
	{
		//插入到parent的右边
		parent->_right = cur;
		cur->_parent = parent;
	}

	//3、更新平衡因子,如果出现不平衡,则需要进行旋转
	while (cur != _root) //最坏一路更新到根结点
	{
		if (cur == parent->_left) //parent的左子树增高
		{
			parent->_bf--; //parent的平衡因子--
		}
		else if (cur == parent->_right) //parent的右子树增高
		{
			parent->_bf++; //parent的平衡因子++
		}
		//判断是否更新结束或需要进行旋转
		if (parent->_bf == 0) //更新结束(新增结点把parent左右子树矮的那一边增高了,此时左右高度一致)
		{
			break; //parent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
		}
		else if (parent->_bf == -1 || parent->_bf == 1) //需要继续往上更新平衡因子
		{
			//parent树的高度变化,会影响其父结点的平衡因子,需要继续往上更新平衡因子
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == -2 || parent->_bf == 2) //需要进行旋转(此时parent树已经不平衡了)
		{
			if (parent->_bf == -2)
			{
				if (cur->_bf == -1)
				{
					RotateR(parent); //右单旋
				}
				else //cur->_bf == 1
				{
					RotateLR(parent); //左右双旋
				}
			}
			else //parent->_bf == 2
			{
				if (cur->_bf == -1)
				{
					RotateRL(parent); //右左双旋
				}
				else //cur->_bf == 1
				{
					RotateL(parent); //左单旋
				}
			}
			break; //旋转后就一定平衡了,无需继续往上更新平衡因子(旋转后树高度变为插入之前了)
		}
		else
		{
			assert(false); //在插入前树的平衡因子就有问题
		}
	}

	return true; //插入成功
}

AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
1.验证其为二叉搜索树
如果中序遍历得到一个有序的序列,就说明为二叉搜索树

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

2.验证其为平衡树
每个结点子树高度差的绝对值不超过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树的查找

跟二叉搜索树的查找方式一样:

//查找函数
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_kv.first) //key值小于该结点的值
		{
			cur = cur->_left; //在该结点的左子树当中查找
		}
		else if (key > cur->_kv.first) //key值大于该结点的值
		{
			cur = cur->_right; //在该结点的右子树当中查找
		}
		else //找到了目标结点
		{
			return cur; //返回该结点
		}
	}
	return nullptr; //查找失败
}

AVL树的性能

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

  • 25
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值