C++——关联式容器(2):AVL树(平衡二叉树)

2.AVL树

2.1 AVL树的概念

        在学习了二叉搜索树后,我们发现了二叉搜索树可以根据大小比较来进行类似于折半查找的操作,使得搜索时间复杂度达到logn的水准。但是在面对极端情况下,如近似有序的序列,那么整棵树的时间复杂度就有可能退化成O(N)。

        为了避免这种极端的情况,同时为了保证搜索效率一定是二分查找法的logn, 我们希望对二叉树进行一些限制,使得其看起来更匀称一些。因此我们给出AVL树的特征:AVL树的任意结点的左右子树高度差不超过1。我们一般使用平衡因子的概念来标识高度差,即

结点的平衡因子=右树高度-左树高度

平衡因子的绝对值<=1

2.2 AVL树详解

2.2.1 pair介绍

        在搜索二叉树的学习中,我们接触到了两种形式——K模型和KV模型。其中KV模型实际上就是将两类值看作是一个整体,称之为一对键值对。当时我们是通过在搜索二叉树结点中定义出_key和_value两个成员变量来分别表示键值对中的key和value。

        在此处我们引入C++库中为我们提供的键值对类:pair。

 

        pair是一个模板类,两个参数分别表示键值对的两个值各自的类型,而其成员first和second则可以用于访问键值对的值。使用很简单,我们在AVL树的实现中将使用pair来作为结点的值。

        除了pair之外,再介绍一个和pair密切相关的函数——make_pair,它接受两个任意类型的参数,返回一个pair类型的对象,因此一般会用它来便捷创建pair对象。

2.2.2 AVL树的实现

2.2.2.1 AVL树的结点

        AVL树的结点应该包括:pair对象——用于存储数据;平衡因子——用于维护AVL树;左孩子、右孩子、父结点的指针——因为可能需要向上调整AVL树的结构,所以需要父结点指针。

	//AVL树的结点
	template<class K, class V>
	struct AVLTreeNode {
		//pair是一个结构体类型,它将一对值组合在一起,作为一个值
		//pair<T1,T2>作为结构体模板,T1和T2分别表示一对的两个类型
		//可以通过.first和.second来访问两个值
		pair<K, V> _pair;
		AVLTreeNode<K, V>* _left;
		AVLTreeNode<K, V>* _right;
		AVLTreeNode<K, V>* _parent;
		int _bf;//平衡因子=右子树高度-左子树高度

		AVLTreeNode(pair<K, V> kv)
			:_pair(kv)
			, _left(nullptr)
			, _right(nullptr)
			, _parent(nullptr)
			, _bf(0)
		{}
	};
2.2.2.2 默认成员函数

        在学会了二叉树的基本操作后,对于AVL树的简单操作并不难上手。拷贝构造可以通过前序遍历的方式创建结点并链接成为一棵树;赋值重载可以复用拷贝构造;析构函数则使用后序遍历的方法来析构结点。

	template<class K, class V>
	class AVLTree {
		typedef AVLTreeNode<K, V> AVLNode;
	public:
		//无参构造
		AVLTree()
			:_root(nullptr)
		{}

		//拷贝构造
		AVLTree(const AVLTree<K, V>& avl)
		{
			_root = copy(avl._root);
		}
	private:
		AVLNode* copy(AVLNode* root)
		{
			if (root == nullptr) return nullptr;
			AVLNode* newnode = new AVLNode(root->_pair);
			newnode->_left = copy(root->_left);
			newnode->_right = copy(root->_right);
			return newnode;
		}

		//赋值重载
	public:
		AVLTree<K, V>& operator=(const AVLTree<K, V> avl)
		{
			swap(this->_root, avl._root);
			return *this;
		}

		//析构
		~AVLTree()
		{
			Destroy(_root);
			_root = nullptr;
		}
	private:
		void Destroy(AVLNode* root)
		{
			if (root == nullptr) return;
			Destroy(root->_left);
			Destroy(root->_right);
			delete root;
		}
	private:
		AVLNode* _root;
    };
2.2.2.3 查找函数

        因为平衡二叉树也是一颗搜索二叉树,所以可以通过比较key的方法来完成查找。

	public:
		AVLNode* Find(const K& key)
		{
			AVLNode* cur = _root;
			while (cur)
			{
				if (cur->_pair.first < key)
				{
					cur = cur->_right;
				}
				else if (cur->_pair.first > key)
				{
					cur = cur->_left;
				}
				else
				{
					return cur;
				}
			}
			return nullptr;
		}

2.3 插入数据——旋转

        插入操作可以说是AVL树的重头戏了,因为AVL树维持其平衡的特征正是依靠每一次插入数据打破平衡后的调整逻辑,我们跟随着插入的逻辑一步步走。

2.3.1 第一步——插入结点

        这部分操作方式和二叉搜索树相同,即根据key的大小关系将待插入的结点插入到树的对应位置处。

2.3.2 第二步——调整平衡因子

        平衡因子=右树高度-左树高度,因为插入了新的结点会影响到树高度,所以接下来就是调整父结点的平衡因子。根据平衡因子的计算方法,当新结点位于左树则平衡因子减1,位于右树则平衡因子加1。

        以左右对称来看,在未插入结点之前的平衡因子是合法的,即可能是0或±1。在插入平衡因子后平衡因子发生改变的情况不同,所导致的结果和处理方式也不同。下面就讨论一下三种不同情况下的处理方式。

        a.平衡因子由0->±1:

        这种情况下,结点插入在了父结点左右任意一边,会导致层数增加一层,因此使得父结点平衡因子发生改变。是否向上产生影响取决于插入结点是否影响到了父结点所在子树的高度,因为这个高度是计算父结点上一层平衡因子的数据来源。因为0->±1的平衡因子变化一定是新增高度带来的结果,所以就必然会影响到上一层的平衡因子,所以需要向上更新。

        向上更新的思路就是将现在的父结点作为新插入的结点,因为新插入结点对于父结点来说就是这一侧子树高度+1,而已经知道父结点高度+1了,那么将其认为是新插入的结点就是合理的。对上层结点依旧采取同样的平衡因子修正判断方法即可。

        b.平衡因子由±1->0:

        在这种情况下,结点插入在了父结点子树原本空缺的一边,父结点子树高度没有发生变化所以就不需要向上更新祖先的平衡因子。

        c.平衡因子由±1->±2:

        这种情况表明新结点插入在了原本突出的一边,导致违反了平衡因子绝对值小于等于1的规则,因此需要进行一些操作来修正这种情况,使其重回AVL树。采取的方法即是旋转,而根据结点的不同布局形式,一共分为4种情况来分类讨论。我们以R代表右,L代表左。

        注:可以通过网上搜索动态图来更加直观理解旋转。

        ①RR失衡——左单旋

        RR即为根->right->right的形状。当出现RR情况的失衡时,就需要进行左单旋,在平衡因子角度就是2和1形式的失衡。

        为了方便描述左单旋的过程,我们为关键结点起个名字如下图。parent是父结点也是bf=2的结点,subR是parent的右孩子,subRL则是subR的左孩子(图中隐含在左树当中)。

        左单旋就是以subR为枢纽,以parent为悬臂向左(逆时针)旋转。旋转后结点之间的父子关系要发生变化,看似复杂实际上涉及到变化的结点有四个:parent,subR,subRL和parent的父结点(称为grandparent),而旋转实际上也就是处理这四个结点间的链接关系,笔者认为不必细说,具体可以参考图片和代码理解。

//左单旋
void RotateL(AVLNode* parent)
{
	AVLNode* subR = parent->_right;
	AVLNode* subRL = subR->_left;
	
	//结点链接三组:subR和parent、parent和sunRL、parent->_parent和subR
	subR->_left = parent;
	parent->_right = subRL;
	if (parent->_parent == nullptr)
	{
		_root = subR;
	}
	else if (parent->_parent->_left == parent)
	{
		parent->_parent->_left = subR;
	}
	else
	{
		parent->_parent->_right = subR;
	}

	subR->_parent = parent->_parent;
	parent->_parent = subR;
	if (subRL)	//右左子树为空树
		subRL->_parent = parent;

	//平衡因子修正
	//可以证明,在左旋处理后,左右subR和parent结点的平衡因子均为0,而且左旋后的子树与插入前相比高度不变,所以该子树的祖先结点平衡因子均不变
	subR->_bf = parent->_bf = 0;
}

        在旋转结束后要记得更新结点的平衡因子。上图中以一个参数h(h>=0)来表示各个子树的高度,因为合理的AVL子树在此处不影响旋转,所以抽象表示,而且为了满足2和1的平衡因子,所画三个子树一定是一样的高度。通过观察上图,可以看到在旋转之后的两个结论:①subR和parent的bf均变为了0;②整棵子树的高度前后都是h+2,这说明了没有影响祖先,不用向上更新平衡因子

         ②LL失衡——右单旋

 

        LL即为根->left->left的形状。当出现LL情况的失衡时,就需要进行右单旋,在平衡因子角度就是-2和-1形式的失衡。

        同样的,出于方便描述左单旋的过程,我们为关键结点起个名字如下图。parent是父结点也是bf=-2的结点,subL是parent的左孩子,subLR则是subL的右孩子(图中隐含在右树当中)。

        右单旋和左单旋就是镜像对称。右单旋以subL为枢纽,以parent为悬臂向右(顺时针)旋转。旋转后变化的结点有四个:parent,subL,subLR和parent的父结点(称为grandparent)。

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

			//结点链接三组:subL和parent、parent和sunLR、parent->_parent和subL
			subL->_right = parent;
			parent->_left = subLR;
			if (parent->_parent == nullptr)
			{
				_root = subL;
			}
			else if (parent->_parent->_left == parent)
			{
				parent->_parent->_left = subL;
			}
			else
			{
				parent->_parent->_right = subL;
			}

			subL->_parent = parent->_parent;
			parent->_parent = subL;
			if (subLR)	//左右子树为空树
				subLR->_parent = parent;

			//平衡因子修正
			//可以证明,在右旋处理后,左右subL和parent结点的平衡因子均为0,而且右旋后的子树与插入前相比高度不变,所以该子树的祖先结点平衡因子均不变
			subL->_bf = parent->_bf = 0;
		}

        最后更新结点的平衡因子。同样的道理,通过观察上图,可以看到:①subL和parent的bf均变为了0;②整棵子树的高度前后都是h+2,没有影响祖先,不用向上更新平衡因子

        ③LR失衡——左右双旋

        LR即为根->left->right的形状。当出现LR情况的失衡时,平衡因子是-2和1的形式,此时发现无论向左还是向右旋转都无法解决失衡的问题,于是引出了双旋的处理方法。双旋,顾名思义就是旋转两次,当发生了如上的LR失衡则需要左右双旋。

        关键结点依然是三个,parent是父结点也是bf=-2的结点,subL是parent的左孩子,subLR则是subL的右孩子(图中隐含在右树当中)。第一次的左旋以subL为轴;第二次的右旋则以parent为轴。因为双旋实际就是两次单旋,因此可以调用单旋的接口完成双旋,此处不再过多解释。

        值得注意的是双旋的平衡因子的更新。与单旋不同,双旋新结点位于左右树的位置不同使得subLR的平衡因子也不尽相同,导致会对最后结果中的平衡因子产生影响。通过讨论,可以得到三种情况下的分布,实际上就是subLR平衡因子为-1(插入在subLR的左子树)、1(插入在subLR的右子树)、0(subLR自身)。上图已经给出了-1情况的解析,下面给出另外两种情况。

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

			//因为双旋调用单旋的函数,会改变平衡因子。单旋考虑的仅是需要单旋情况的平衡因子改变,我们此处只需要单旋的旋转操作而不接受其平衡因子的改变
			//所以在之后会重新进行双旋操作的平衡因子修正,但是为了确定修正方案,并且单旋会改变平衡因子,所以需要提前记录可以作为标识的平衡因子作为分支语句的条件
			int bf = subLR->_bf;

			RotateL(subL);
			RotateR(parent);

			//平衡因子修正
			//①subLR的左子树插入新结点——subLR的平衡因子为-1
			if (bf == -1)
			{
				parent->_bf = 1;
				subL->_bf = subLR->_bf = 0;
			}
			//②subLR的右子树插入新结点——subLR的平衡因子为1
			else if (bf == 1)
			{
				parent->_bf = subLR->_bf = 0;
				subL->_bf = -1;
			}
			//③subLR自身是被插入的新结点——subLR的平衡因子为0
			else
			{
				parent->_bf = subL->_bf = subLR->_bf = 0;
			}
		}

        最后更新结点的平衡因子,结果如代码和图片所示。可以看到整棵子树的高度前后都是h+2,没有影响祖先,不用向上更新平衡因子

        ④RL失衡——右左双旋 

 

        RL即为根>right->left-的形状。当出现RL情况的失衡时,平衡因子是2和-1的形式,需要右左双旋。

        关键结点依然是三个,parent是父结点也是bf=2的结点,subR是parent的左孩子,subRL则是subR的左孩子(图中隐含在左树当中)。第一次的右旋以subR为轴;第二次的左旋则以parent为轴。

        同样的,根据新结点位于左右树的位置不同,subRL的平衡因子也不尽相同,最后结果中的平衡因子也有三种情况:subRL平衡因子为-1(插入在subRL的左子树)、1(插入在subRL的右子树)、0(subRL自身)。上图已经给出了-1情况的解析,下面给出另外两种情况。

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

			//因为双旋调用单旋的函数,会改变平衡因子。单旋考虑的仅是需要单旋情况的平衡因子改变,我们此处只需要单旋的旋转操作而不接受其平衡因子的改变
			//所以在之后会重新进行双旋操作的平衡因子修正,但是为了确定修正方案,并且单旋会改变平衡因子,所以需要提前记录可以作为标识的平衡因子作为分支语句的条件
			int bf = subRL->_bf;

			RotateR(subR);
			RotateL(parent);

			//平衡因子修正
			//①subRL的左子树插入新结点——subRL的平衡因子为-1
			if (bf == -1)
			{
				subR->_bf = 1;
				parent->_bf = subRL->_bf = 0;
			}
			//②subRL的右子树插入新结点——subRL的平衡因子为1
			else if (bf == 1)
			{
				subR->_bf = subRL->_bf = 0;
				parent->_bf = -1;
			}
			//③subRL自身是被插入的新结点——subRL的平衡因子为0
			else
			{
				parent->_bf = subR->_bf = subRL->_bf = 0;
			}
		}

        最后更新结点的平衡因子,结果如代码和图片所示。可以看到整棵子树的高度前后都是h+2,没有影响祖先,不用向上更新平衡因子

2.3.3 插入函数代码

        最后给出上述插入结点和调整平衡因子主函数的代码。其思路我们已经分析过,调用的旋转接口也一一解释,读者可根据详细的注释理解。

		//插入
	public:
		bool Insert(const pair<K, V>& kv)
		{
			//第一个结点特殊处理
			if (_root == nullptr)
			{
				_root = new AVLNode(kv);
				return true;
			}

			AVLNode* cur = _root;
			AVLNode* parent = nullptr;
			while (cur)
			{
				if (cur->_pair.first > kv.first)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_pair.first < kv.first)
				{
					parent = cur;
					cur = cur->_right;
				}
				else
					return false;
			}
			cur = new AVLNode(kv);
			if (parent->_pair.first > kv.first)
				parent->_left = cur;
			else
				parent->_right = cur;
			cur->_parent = parent;
			
			//更新平衡因子
			while (parent)
			{
				//parent的平衡因子:在左插入时-1,右插入时+1
				if (cur == parent->_left)
					parent->_bf--;
				else
					parent->_bf++;

				//①parent的平衡因子0->±1,表示结点插入在左右任意一边,该子树实际上新增了一层,需要向上更新
				if (parent->_bf == 1 || parent->_bf == -1)
				{
					//因为插入结点使得子树变高了一层,所以祖先也可以根据左插入-1,右插入+1的规则进行平衡因子的修改,所以变量向上传递一层即可
					cur = parent;
					parent = parent->_parent;
				}
				//②parent的平衡因子±1->0,表示结点插入在空缺的一边,没有新增层数,不需要向上更新
				else if (parent->_bf == 0)
				{
					break;
				}
				//③parent的平衡因子±1->±2,表示结点插入在已经盈余的一边,不再满足AVL树的要求,需要旋转处理
				else
				{
					//旋转处理
					//左单旋:本就突出的右子树的右子树插入新结点,形成RR的形状破坏平衡
					//以subR为中心,subR的父结点parent为悬臂左旋,那么parent与其左子树将成为subR的左子树,而subR原先的左子树subRL将成为旋转下来的父结点的右子树
					//这种RR的情况下,parent的平衡因子为2,subR的平衡因子为1
					if (parent->_bf == 2 && cur->_bf == 1)
					{
						RotateL(parent);
					}
					//右单旋:本就突出的左子树的左子树插入新结点,形成LL的形状破坏平衡
					//以subL为中心,subL的父结点parent为悬臂右旋,那么parent与其右子树将成为subL的右子树,而subL原先的右子树subLR将成为旋转下来的父结点的左子树
					else if (parent->_bf == -2 && cur->_bf == -1)
					{
						RotateR(parent);
					}
					//左右双旋:本来突出的左子树的右子树插入新结点,形成LR的形状破坏平衡
					//对于这种情况,需要先左旋再右旋,左旋针对subL,完成后再对parent右旋
					else if (parent->_bf == -2 && cur->_bf == 1)
					{
						RotateLR(parent);
					}
					//右左双旋:本来突出的右子树的左子树插入新结点,形成RL的形状破坏平衡
					//对于这种情况,需要先右旋再左旋,右旋针对subR,完成后再对parent左旋
					else if(parent->_bf == 2 && cur->_bf == -1)
					{
						RotateRL(parent);
					}
					else
					{
						assert(0);
					}

					//因为旋转情况的判断与执行发生在插入新结点后的子树,在旋转后,该子树的高度与未插入结点时的高度一致,因此无需对子树的祖先进行平衡因子修正,可以直接结束循环
					break;
				}
			}
			return true;
		}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

犀利卓

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

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

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

打赏作者

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

抵扣说明:

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

余额充值