前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >我知道二叉树一定满足不了你,接下来上场的是[平衡二叉树之一(AVL树)]

我知道二叉树一定满足不了你,接下来上场的是[平衡二叉树之一(AVL树)]

作者头像
薛定谔方程难
发布2024-07-13 09:00:00
630
发布2024-07-13 09:00:00
举报
文章被收录于专栏:我的C语言

我不去想未来是平坦还是泥泞,只要热爱生命一切,都在意料之中。——汪国真

1、诞生原因

已经有了二叉树了,那为什么我们需要去使用平衡二叉树这种类型呢? 其实原因还是在于,由于特殊情况的存在,二叉树不能真正的做到对所有的数据都能够优化,有时候处理的结果还不如不处理的结果,就例如在这篇文章中的所介绍的二叉树一样,其中的缺点也是显而易见的(直接点可以看到之前的文章)。 由于二叉树的本身缺陷,如果树中的元素接近有序或者是有序,都会造成二叉搜索树的大大退化,进一步可能成为单支树,时间复杂度退化成O(N)。 所以为了满足这种特别的情况,我们需要一些在二叉树基础上的改变。需要在二叉树的基础上加一些限制来合理的改变二叉树结构,让原本可能只形成单只的二叉树得到相对于的处理,使其变换原本的形态,但不改变二叉树的基本限制。使其具有更加方便与搜索等一系列操作的结构。来实现二叉树这种数据结构的更加完美,更能符合各种情况。 这样的话就需要 AVLTree和RBTree来帮助实现。

2、什么是AVL树

由于二叉搜索树在面对一些数据时,会退化并且还会降低搜索效率。因此,俄罗斯的两个数学家G.M.Adelson-Velskii和E.M.Landis 在1962年发明了一种能够解决问题的方法。 当像二叉搜索树中插入节点之后,如果能够保证每个结点左右子树高度绝对值差不超过1(需要进行旋转调整,当超过1的时候),即可降低树的高度,从而减少平均搜索长度,提高搜索效率。 总结来说,AVL树具有以下的特点 1、每一个节点的左右子树都是AVL树 2、左右子树的 高度差/平衡因子 的绝对值不能超过1 此后都会说成平衡因子—右子树高度减去左子树的高度

这样即使是多么样子的数据,都会成指数倍的减少,大大的减少了搜索的时间,提升了效率。

3、如何设计AVL树

3、1、AVL树节点的定义

由于AVL树的特点,简单二叉树节点的定义反而是不能满足我们AVL树的要求。 由于平衡因子的出现,我们必须找到一个合适的方法来存储平衡因子并且帮助我们能够判断每一个节点的位置是否平衡。 所以,这样的节点设计,我觉得是非常好的。

代码语言:javascript
复制
template<class T>
struct AVLTreeNode
 {
 	AVLTreeNode(const T& data)
     : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
      ,_data(data), _bf(0)
    {}
 	AVLTreeNode<T>* _pLeft;   // 该节点的左孩子
	AVLTreeNode<T>* _pRight;  // 该节点的右孩子
	AVLTreeNode<T>* _pParent; // 该节点的双亲
	T _data;
    int _bf; 
};

3、2、AVL树的插入

二叉搜索树的插入很简单,AVL树的插入有难但是也有简单的。 插入规则首先按照二叉搜索树那样,把节点插入。然后呢,在考虑节点的平衡因子改变的办法。 比如说如果有一个平衡因子是0的话,无论插入左边还是右边都不会超过AVL树的定义。所以不需要再作别的操作,只需要向上更新节点的平衡因子就行。 但是如果一个节点的平衡因子不是0,而是1或者-1,这两种情况下,只有在一边插入让平衡因子重新回到0才不会有旋转的影响,但是如果平衡因子变成-2或者是2的话,就要进行旋转调整。然后再进行平衡因子的调整。 平衡因子改变的情况: A)插入父亲节点的左边,父亲节点平衡因子– B)插入父亲节点右边,父亲节点的平衡因子++ 平衡因子改变造成的操作 1、如果父亲节点平衡因子 == 0,那就是说明父亲坐在节点的子树已经到平衡,不需要再向上更新,可以直接结束。 2、如果父亲节点的平衡因子 == -1/1,此时父亲节点高度改变,需要向上更新。 3、如果父亲平衡因子==-2/2,那么意味着父亲所在子树已经不平衡,需要旋转处理 旋转处理是一个很重要的环节,在下面会单独介绍。

代码语言:javascript
复制
//更新平衡因子
while (parent)
		{
			if (cur == parent->_left)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			if (parent->_bf == 0) // 1 -1 -> 0
			{
				// 更新结束
				break;
			}
			else if(parent->_bf == 1 || parent->_bf == -1)  // 0 -> 1 -1
			{
				// 继续往上更新
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2) // 1 -1 -> 2 -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)
				{
					RotateRL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				
				break;
			}
			else
			{
				// 理论而言不可能出现这个情况
				assert(false);
			}
		}

3、3、平衡因子那些事

关于插入后平衡因子变成-1/1的那些节点不需要太多的关注或者换句话说应该是只需要向上更新到当时的父亲节点的平衡因子为0就能够停止==(如果过程中遇不见平衡因子为-2/2的情况下的话)。== 如果不幸遇到平衡因子是-2/2的情况的话。

3、3、1、平衡因子-2/2下的简单情况

如果说一个平衡因子是-2/2,那么必不可少的是需要旋转,可是旋转过程也是有简单过程的也有复杂过程的。 对于一个左左或者是右右的情况是简单的 什么是左左,右右? 简单来说,请看图配合理解。 首先是第一种左左

其次是右右

当然这是抽象图,画一个大概,但是其实换一句话说,这也只能通过抽象图来画,因为当h的变化,越来越大的h会有上千上万种可能。 无疑例外需要旋转,对于第一个来说,需要根据RotateR一下,参数是图中的parent节点。 在RotateR之前是先从插入的cur节点开始一步一步向上更新。然后再更新的过程中找到了一个cur的parent节点的平衡因子已经达到了-2/2

代码语言:javascript
复制
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;
		}

		parent->_bf = subL->_bf = 0;
	}

其中更新_bf实在所有移动之后进行的。能够蒋将最后一步的_bf程序化进行,那就是说明这最后一步对于所有要左左的情况下旋转都能适应。

那么关于右右的情况的话,也就是和左左相似。

代码语言:javascript
复制
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->_right == parent)
			{
				ppNode->_right = subR;
			}
			else
			{
				ppNode->_left = subR;
			}
			subR->_parent = ppNode;
		}

		parent->_bf = subR->_bf = 0;
	}
3、3、2、平衡因子-2/2下的复杂情况

这种复杂的情况就是和两个简单的相反的情况,就例如说右左,左右这种情况。 为什么这种情况会和之前不一样呢? 就比如说是右左的情况来看一看究竟。

这就是右左情况之下的单纯的只是使用RotateL和RotateR。这样是会是作茧自缚,到最后还是不能成功解决问题,反而还是回到了最初的状态。 所以此时就需要在特殊情况下的特殊判断的特殊处理! 先将cur节点执行一次RotateR调整一下

就像这样,只有这样子之后才能到RotateL情况,以parent为参数传入。完成右左的特殊情况。左右的话和右左相差不大,能够根据右左来写出左右的情况。

代码语言:javascript
复制
void RotateRL(Node* parent)
	{
		RotateR(parent->_right);
		RotateL(parent);
	}

4、总结

根据上面的判断,能够得出所有平衡因子不同情况下,如何变为正常的,符合AVL树的定义的操作。 值得注意的是需要旋转,只有在旋转的特殊情况之下,我们才能够解决简单的搜索二叉树的缺点,虽然旋转很难,但是通过旋转的操作,我们能够大大提升对于任何数据的搜索的性能,提高效率。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-07-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、诞生原因
  • 2、什么是AVL树
  • 3、如何设计AVL树
    • 3、1、AVL树节点的定义
      • 3、2、AVL树的插入
        • 3、3、平衡因子那些事
          • 3、3、1、平衡因子-2/2下的简单情况
          • 3、3、2、平衡因子-2/2下的复杂情况
      • 4、总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档