C++数据结构——AVL树

前言:本篇文章将紧随二叉搜索树的节奏,分享一个新的数据结构——AVL树。


目录

一.AVL树概念

二.AVL树插入规则

三.AVL树实现

1.基本框架

2.插入

3.旋转

1)左\右单旋

2)左右/右左双旋

4.遍历

5.求树高度

6.判断平衡

7.求树高度

总结


一.AVL树概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。

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

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

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


二.AVL树插入规则

由于AVL树的独特结构,我们给出以下的插入规则: 

1.按照搜索树规则插入。

2.更新插入节点的祖先节点的平衡因子:

        a.插入父亲的左边,父亲的平衡因子--

        b.插入父亲的右边,父亲的平衡因子++

        c.父亲的平衡因子 == 0,父亲所在的子树高度不变,不再往上更新,插入结束。

        d.父亲平衡因子 == 1 or -1,父亲所在的子树高度变了,往上更新,重复以上步骤。

        e.父亲平衡因子 == 2 or -2,父亲所在的子树已经不平衡了,需要旋转处理


三.AVL树实现

1.基本框架

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

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

template<class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
private:
	Node* _root;
};

基本框架与平衡二叉树类似,区别在于AVL树的节点为键值对

同时我们还需增加平衡因子_bf和父节点_parent,方便我们进行调整。


2.插入

	//插入
	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
				return false;
		}
		//插入
		cur = new Node(kv);
		if (kv.first < parent->_kv.first)
			parent->_left = cur;
		else
			parent->_right = cur;

		cur->_parent = parent;
		//更新平衡因子


		return true;
	}

基本的插入步骤与平衡二叉树一模一样,需要关注的就是插入的节点变为键值对

下面我们单独来看如何更新平衡因子

        while (parent)
		{
			if (cur == cur->_parent->_left)
				parent->_bf--;
			else
				parent->_bf++;

			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)
			{
				//出现问题,进行旋转
				break;
			}
			else
				assert(false);
		}

按照我们上边的规则其实很好写出上述代码,要注意循环条件为parent如果没有父亲,也就是到达了根节点,那就无法再进行调整。 

下面我们来重点关注,如何进行旋转


3.旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

  1.  新节点插入较高左子树的左侧---左左:右单旋。
  2.  新节点插入较高右子树的右侧---右右:左单旋。
  3. 新节点插入较高左子树的右侧---左右:先左单旋再右单旋。
  4. 新节点插入较高右子树的左侧---右左:先右单旋再左单旋。

下面我们就一一来看这四种情况。


1)左\右单旋

先来看右单旋,可以抽象理解为,左子树过高,需要向右边旋转拉低。 

从上图能够看出,右单旋的步骤为:

  1. 让平衡因子为-2的节点成为它的左子节点的右子节点;
  2. 同时让该左子节点的右子节点成为平衡因子为-2的节点的左子节点。

同时我们需要关注的细节是:

  • 平衡因子为-2的节点是否为根节点。如果不是根节点则需要调整其父节点的指向。
  • 左子节点的右子节点是否为空。

通过这样的调整,就可以实现平衡,同时调整的两个关键节点的平衡因子均归0。 

下面来看代码:

	void RotateR(Node* parent)
	{
		//定义左子节点
		Node* subL = parent->_left;
		//定义左子节点的右子节点
		Node* subLR = subL->_right;
		//调整
		parent->_left = subLR;
		//判空
		if (subLR)
			subLR->_parent = parent;
		//调整
		subL->_right = parent;
		Node* ppNode = parent->_parent;
		parent->_parent = subL;
		if (parent == _root)//判断是否为根
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else//不是根节点,调整父节点指向
		{
			if (ppNode->_left == parent)
				ppNode->_left = subL;
			else
				ppNode->_right = subL;

			subL->_parent = ppNode;
		}
		//平衡因子归0
		parent->_bf = subL->_bf = 0;
	}

再来看左单旋: 

 左单旋则与右单旋完全相反,所以我们不做过多解释,直接给出代码:

	//左单旋
	void RotateL(Node* parent)
	{
		//定义右子节点
		Node* subR = parent->_right;
		//定义右子节点的左子节点
		Node* subRL = subR->_left;
		//调整
		parent->_right = subRL;
		//判空
		if (subRL)
			subRL->_parent = parent;
		//调整
		subR->_left = parent;
		Node* ppNode = parent->_parent;
		parent->_parent = subR;
		if (parent == _root)//判断是否为根
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else//不是根节点,调整父节点指向
		{
			if (ppNode->_left == parent)
				ppNode->_left = subR;
			else
				ppNode->_right = subR;

			subR->_parent = ppNode;
		}
		//平衡因子归0
		parent->_bf = subR->_bf = 0;
	}

2)左右/右左双旋

如果说树并不是子树的一条斜边独高,而是折线型的一颗子树高,此时单靠单旋是解决不了问题的,因此需要通过双旋来解决

上图所示为先左后右的折线型,所以我们需要进行左右双旋,步骤为:

  1. 先从折线的折点位置,即上图的30位置,进行左单旋,使树变为左边一条斜边独高的树。
  2. 在从折线起点位置进行右单旋。
  3. 更新平衡因子。

其中更新平衡因子也分为不同的情况,以上图为例:

  • 如果新节点插入位置为60的左,那么旋转后60为0,30为0,90为1。
  • 如果新节点插入位置为60的右,那么旋转后60为0,30为-1,90为0。
  • 如果新节点就是60,那么三者的平衡因子均为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)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

 注意更新平衡因子是通过初始时折线末点的平衡因子判断的,所以要提前记录。


再来看右左双旋

 与左右双旋相反,右左双旋是先右后左的折线,所以其操作步骤与之相反:

  1. 先从折线的折点位置,即上图的90位置,进行右单旋,使树变为右边一条斜边独高的树。
  2. 在从折线起点位置进行左单旋。
  3. 更新平衡因子。

其中更新平衡因子也同样分为不同的情况,以上图为例:

  • 如果新节点插入位置为60的左,那么旋转后60为0,30为0,90为1。
  • 如果新节点插入位置为60的右,那么旋转后60为0,30为-1,90为0。
  • 如果新节点就是60,那么三者的平衡因子均为0。

代码如下:

	//右左双旋
	void RotateLR(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		RotateR(parent->_right);
		RotateL(parent);
		if (bf == -1)
		{
			subR->_bf = 1;
			subRL->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			subR->_bf = 0;
			subRL->_bf = 0;
			parent->_bf = -1;
		}
		else if (bf == 0)
		{
			subL->_bf = 0;
			subLR->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

根据父节点及其左右子节点的平衡因子,即可判断对应的旋转方式,下面补充插入步骤:

 

			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//出现问题,进行旋转
				//左单旋
				if (parent->_bf == -2 && parent->_left->_bf == -1)
				{
					RotateL(parent);
				}
				//右单旋
				else if (parent->_bf == 2 && parent->_right->_bf == 1)
				{
					RotateR(parent);
				}
				//左右单旋
				else if (parent->_bf == -2 && parent->_left->_bf == 1)
				{
					RotateLR(parent);
				}
				//右左单旋
				else
				{
					RotateRL(parent);
				}
				break;
			}

4.遍历

遍历操作与二叉搜索树类似,需要修改的是我们需要将键值对均打印出来:

	//遍历
	void InOrder()
	{
		inOrder(_root);
		cout << endl;
	}
	void inOrder(const Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		inOrder(root->_left);
		cout << root->_kv.first << ':' << root->_kv.second << " ";
		inOrder(root->_right);
	}

为了方便调用函数而无需传参,我们采用如上方式进行代码编写。 


5.求树高度

求树高度我们前边在讲解二叉树的时候已经分享过了,只需求出左右子树高度的最大值+1即可,通过递归计算:

	//求树高度
	int Height(const Node* root)
	{
		if (root == nullptr)
			return 0;
		return max(Height(root->_left), Height(root->_right)) + 1;
	}

6.判断平衡

判断树是否平衡,即判断两棵子树的高度差是否大于等于2

	//判断平衡
	bool IsBalance()
	{
		return isBalance(_root);
	}
	bool isBalance(const Node* root)
	{
		if (root == nullptr)
			return true;

		int leftHeight = Height(root->_left);
		int rightHeight = Height(root->_right);
		if (abs(leftHeight - rightHeight) >= 2)
			return false;
		//检查平衡因子
		if (rightHeight - leftHeight != root->_bf)
			return false;

		return isBalance(root->_left) && isBalance(root->_right);
	}

同时还需要通过递归来判断各个子树是否平衡


7.求树高度

求树的大小,通过递归即求左子树的大小+右子树的大小+根节点:

	//求树大小
	int Size()
	{
		return size(_root);
	}
	int size(const Node* root)
	{
		if (root == nullptr)
			return 0;
		return size(root->_left) + size(root->_right) + 1;
	}

总结

关于AVL树的基本内容就分享这么多,喜欢本篇文章的小伙伴记得一键三连,我们下期再见!

  • 71
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 61
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

很楠不爱

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

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

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

打赏作者

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

抵扣说明:

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

余额充值