室友对我说AVL树插入,旋转的水太深了,你把握不住的。

这里是引用
说在前面的话:
🍁🍁室友说面试了AVL树的旋转,我想了一下得赶紧搞篇博客巩固巩固
🍁🍁本篇博客重点搞AVL树的旋转

AVL树的概念

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

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
    在这里插入图片描述
    注意:****一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 ,O(log2 N)搜索时间复杂度O( log2N)

AVL树结点的定义

室友建议我用三叉链实现,不用三叉链很麻烦,还需要借助栈比较难搞。博主就搞成三叉链,并引入平衡因子(平衡因子=右子树的高度-左子树的高度)室友还叫我写成KV模型的,那博主还是听从室友的建议。此外还要提供一个构造函数来构造新的结点,构造出来的结点的左右子树为空,将构造的新节点的平衡因子初始化成0即可。

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)
	{}
};

AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为3步:

1.按照二叉搜索树的方式插入新节点
2.调整节点的平衡因子
3.如果出现不平衡则需要进行旋转处理

由于AVL树本身就是二叉搜索树,插入还是和搜索树是一样的
1.当插入结点的key值比当前结点的key值小,则插入到该结点的左子树
2.当插入结点的key值比当前结点的key值大,则插入到该结点的右子树
3.当插入结点的key值等于当前结点的key值,则插入失败
一个结点的平衡因子是否需要更新,取决于该结点的左右子树的高度是否发生变化。当插入一个新的结点它祖先的平衡因子都会收到影响。
在这里插入图片描述

平衡因子的更新规则:

1.新增结点在parent的右边,平衡因子++
2.新增结点在parent的左边,平衡因子- -

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

1.parent的平衡因子等于1或者-1则继续往上更新
2.parent的平衡因子等于0,则停止更新
3.parent的平衡因子等于2或者-2,说明已经不平衡了需要进行旋转处理

分析:
1.parent更新后的平衡因子为1或-1,则说明在parent的右边或者左边新增了结点,从而影响了parent的父亲结点所以要继续往上更新平衡因子。
2.parent更新后的平衡因子为0,1或-1经过++或- -变成0,说明新增结点在parent高的一侧使得parent的左右子树一样高,不会影响parent的父亲结点,就不用往上更新平衡因子。
3.parent更新后的平衡因子为2或-2,parent的左右子树差的绝对值大于1,已经不满足AVL树,需要旋转处理。
最坏的情况是调整到根节点,例如下面的情况:

在这里插入图片描述
假设以parent为根的子树不平衡,那么parent的平衡因子为2或者-2,分为下面的情况考虑:

  1. parent 的平衡因子为2,说明parent的右子树高,设parent的右子树的根subR
    当subR的平衡因子为1时,说明新增结点在subR的右边,需要进行左单旋
    当subR的平衡因子为1时,说明新增结点在subR的左边,需要进行右左双旋
    我问室友有没有好记的办法,他画了一张草图给我:
    在这里插入图片描述
  2. parent 的平衡因子为2,说明parent的右子树高,设parent的右子树的根subL

当subL的平衡因子为-1时,进行右单旋
当subL的平衡因子为1时,进行左右双旋
在这里插入图片描述
我就说你画的真好下次就不要画了吧.具体的旋转细节往下看。
插入的代码如下:

template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	AVLTree()
		:_root(nullptr)
	{}
	bool insert(const pair<K, V>& kv)
	{	//树为空,new一个新节点
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		Node* cur = _root;
		Node* parent = cur;
		while (cur)
		{
			if (cur->_kv.first > kv.first)//插入结点的key值小于当前结点,往左走
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_kv.first < kv.first)//插入结点的key值大于当前结点,往右走
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;//没有找到,返回false
			}
		}
		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			//注意是三叉链,还要链接上parent
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
		     //要链接上parent
			parent->_left = cur;
			cur->_parent = parent;
		}
		//控制平衡
		//1.更新平衡因子
		//2.如果出现不平衡则需要旋转处理
		while (parent)
		{
			//在父亲结点左边插入平衡因子--,右边则++
			if (parent->_left == cur)
				parent->_bf--;
			else
				parent->_bf++;
			//1.平衡因子=0就不要更新平衡因子了
			//2.=1或-1则要继续往上调整
			//3.=2或-2则要旋转处理
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == -1 || parent->_bf == 1)
			{
				//继续向上调整平衡因子
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == -2 || parent->_bf == 2)
			{
				//旋转处理
				if (parent->_bf == -2)
				{
					if (cur->_bf == -1)
					{
						//左边高,右单旋处理
						RotateR(parent);
					}
					else
					{
						//左右双旋
						RotateLR(parent);
					}
				}
				else //parent->_bf == 2
				{
					if (cur->_bf == 1)
					{
						//右边高,左单旋
						RotateL(parent);
					}
					else
					{
						//右左双旋
						RotateRL(parent);
					}
				}
				break;
			}
			else 
			{
				//走到这里就不是旋转的问题,你要检查别的问题
				assert(false);
			}			
		}
		return true;
	}
	private:
	Node* _root;
};

这是插入的整体大逻辑,接下来就要一个一个的实现旋转了。首先得实现左单旋和右单旋,双旋的话就可以调用单旋。

右单旋

具象图
在这里插入图片描述
因为有太多种情况了所以要用抽象图来概括:
将30定义成parebt->_left,b设为shuLR,是subL的右孩子,这样定义的好处是好处理链接关系。
在这里插入图片描述

动图演示:
在这里插入图片描述

需要注意的细节:
在这里插入图片描述

1.subLR可能为空
2.先该30的右给60的左,在让60整棵树左30的右子树。因为是三叉链实现的,所以还要注意修改动的结点的父亲结点
3.60可能是子树,也有可能是单独的树

void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* parentParent = parent->_parent;//记录parent的父亲结点
		//subLR做parent->_left
		parent->_left = subLR;
		subL->_right = parent;
		//同时更新动的2个节点的parent
		//注意subLR还可能是空结点
		if (subLR)
			subLR->_parent = parent;
		parent->_parent = subL;
		//parent可能是单独的树,或者子树,分情况
		if (_root == parent)
		{
			_root = subL;
			_root->_parent = nullptr;
		}

		else
		{
			//还有可能parent是子树,可能是左子树
			if (parentParent->_left == parent)
				parentParent->_left = subL;
			else
				//也可能是右子树
				parentParent->_right = subL;
			//调整subL的父亲结点
			subL->_parent = parentParent;
		} 
		//调整平衡因子
		parent->_bf = subL->_bf = 0;
	}

室友说旋转的细节还是很多的,你要是不注意就会出错,就可能把握不住。

左单旋

有了右单旋的基础后再来搞左单旋的话,可能会轻松一点。还是跟上面的一样,先画具象图,在画抽象图概括所有的情况。
具象图:同样用右旋转的定义方法
在这里插入图片描述
抽象图
在这里插入图片描述
动图演示:
在这里插入图片描述
左单旋代码

void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* parentParent = parent->_parent;
		//先旋转
		parent->_right = subRL;
		subR->_left = parent;

		parent->_parent = subR;
		//在改父亲结点
		if (subRL)
			subRL->_parent = parent;
		if (_root == parent)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		
		else 
		{
			//subR旋转后可能是左右子树2种情况
			if(parentParent->_left == parent)
			   parentParent->_left = subR;			
			else
			  parentParent->_right = subR;
			subR->_parent = parentParent;
		}	
		//调整平衡因子
		parent->_bf = subR->_bf = 0;
	}

细节跟右单旋是一样的,这里就不多做解释了。

右左双旋

双旋的话就直接上抽象图了,具象图感兴趣的小伙伴可以自己动手画,只有自己动手画才可能把握的住。双旋先旋转parent的右子树,在将parent左单旋。旋转的逻辑是简单了,难把握的是平衡因子不好调节了。
在这里插入图片描述

平衡因子分为3种情况调节:

第1种情况:新增结点在c的右边:
在这里插入图片描述

第2种情况:新增结点在b的左边
在这里插入图片描述

第3种情况:结点60就是新增结点:
在这里插入图片描述
总结上面的情况:
需记录subRL的平衡因子,还需保存subR,subRL.
根据subRL的平衡因子在更新其他需要更新的平衡因子
1.当subRL的平衡因子=1时,subR,subRL,parent的平衡因子分别是0,0,-1
2.当subRL的平衡因子=-1时,subR,subRL,parent的平衡因子分别是1,0,0
3.当subRL的平衡因子=0时,subR,subRL,parent的平衡因子的都是0

void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;//记录subRL的平衡因子

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

		// 平衡因子更新
		//分3种情况
		if (bf == 1)
		{
			subR->_bf = 0;
			parent->_bf = -1;
			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);
		}	
	}

图一定要画好,图画完一遍就是理解一遍旋转。代码是次要的。

左右双旋

跟上面的双旋是一样的。这里博主就直接画出三种情况,不做多余的讲解了。
第1种情况:b是新增结点
在这里插入图片描述
第2种情况:c是新增结点
在这里插入图片描述

第3种情况:60就是新增结点
在这里插入图片描述
跟上面的双旋的逻辑是一样的。
总结平衡因子的变化:
根据subLR的平衡因子在更新其他需要更新的平衡因子
1.当subLR的平衡因子=1时,subL,subLR,parent的平衡因子分别是-1,0.0;
2.当subLR的平衡因子=-1时,subL,subLRL,parent的平衡因子分别是0,0,1;
3.当subLR的平衡因子=0时,subL,subLR,parent的平衡因子的都是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)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			subLR->_bf = 0;
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == 0)
		{
			subLR->_bf = 0;
			subL->_bf = 0;
			parent->_bf = 0;
		}
		else
			assert(false);
	}

AVL树的验证

到这里AVL树的旋转才算是结束了。那么接下来就是要测试自己的旋转有没有问题。

#include"AVLtree.h"

int main()
{
	int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16,14};
	//int arr{ 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	AVLTree<int, int> t;
	for (auto  e : arr)
	{
		t.insert(make_pair(e,e));
	}
	
	t.InOrder();
	return 0;
}

用2组特殊的测试用例来测试一波
在这里插入图片描述
到这里只能说明是搜索树没有问题。我们还要增加一个能检测我们的树是否平衡。
1.先计算出左右子树的高度
2.再检测每课子树的平衡因子

bool IsAVLTree()
	{
		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)
		{
			cout << "平衡因子异常:" << root->_kv.first << endl;
			return false;
		}

		return abs(rightHeight - leftHeight) < 2
			&& _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}
	int _Height(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}

		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);

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

在测试中加上IsAVLTree()检测是否是AVL树。
在这里插入图片描述
到这里AVL树的插入才算没问题。
一定要注意细节,一点细节的错误导致整个插入出错。因为博主因为小的失误调试了好几个小时。还要用好编译器的调试功能,能帮助我们很多。

AVL树的查找

AVL树的查找跟搜索树一样很简单
1.若为空树,返回空
2.key值比当前结点的key值大就到右边找
3.key值比当前结点的key值大就到左边找
4.key值和当前结点的key值相等或者没找到返回false

Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return nullptr;
	}

AVL树的修改

修改可以直接调用insert,在上一篇的map中说到:
insert的返回值:

它的返回值是pair<iterator,bool>,第1个类型是迭代器,第2个类型是bool类型
当插入的值的key在map中不存在,则插入成功,返回插入元素的迭代器和true
当插入的值的key在map中已经存在,则插入失败,返回插入元素的迭代器和false

最后直接返回pair对象即可.

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

		// 找到存储位置,把数据插入进去
		Node* parent = _root, * cur = _root;
		while (cur)
		{
			if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return make_pair(cur, true);
			}
		}

		cur = new Node(kv);
		Node* newnode = cur;
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		// 控制平衡
		// 1、更新平衡因子
		// 2、如果出现不平衡,则需要旋转
		//while (parent)
		while (cur != _root)
		{
			if (parent->_left == cur)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				// parent所在的子树高度变了,会影响parent->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)
					{
						// 左单旋
						RotateL(parent);
					}
					else // cur->_bf == -1
					{
						RotateRL(parent);
					}
				}

				break;
			}
			else
			{
				// 插入节点之前,树已经不平衡了,需要检查
				assert(false);
			}
		}

		return make_pair(newnode, true);
	}

这里博主没有实现迭代器,等到用红黑树封装set和map的时候在实现迭代器。
室友跟我说AVL树的删除简单了解就可以了。
AVL树的性能:
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即(log2N) 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。
因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

AVL树的完整代码:
https://gitee.com/ge-xiaoyu/testc-plus/commit/a7b3378b92ab983968a42119c91a480faa10f109

这里是引用
🍁🍁博主水平有限,如有错误,直接留下评论即可!
🍁🍁感觉还是把握住了!
🍁🍁欢迎一键三连!一起把握住AVL树! 🍁🍁

  • 37
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 31
    评论
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_End丶断弦

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

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

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

打赏作者

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

抵扣说明:

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

余额充值