搜索二叉树进阶之AVL树

前言

二叉搜索树(BST)是一种基础的数据结构,能够高效地进行搜索、插入和删除操作。然而,在最坏的情况下,普通的BST可能会退化成一条链表,导致操作效率降低。为了避免这种情况,出现了自平衡二叉搜索树,AVL树就是其中的一种。

一、什么是AVL树?

AVL树是Adelson-Velsky和Landis在1962年发明的一种自平衡二叉搜索树。它的特点是通过对树进行旋转操作来保持平衡,以确保在最坏情况下,树的高度仍然是O(log n),从而保证插入、删除和查找操作的时间复杂度都是O(log n)。

1.1 AVL树的平衡因子

AVL树的核心概念是平衡因子(Balance Factor)。对于树中的任意节点,平衡因子定义为其右子树高度减去左子树高度的值(其实左右都可以,只要保证左右子树的高度差的绝对值小于等于1就行)。即:

  • 平衡因子 = 右子树高度 - 左子树高度

为了保证AVL树的平衡,平衡因子的取值必须是-1、0或1。一旦某个节点的平衡因子超出这个范围,就需要进行旋转操作来恢复平衡。(旋转操作后续讲解)

2.2 AVL树平衡因子的更新

我们规定,当一个节点的右子树高度增加时,此时平衡因子就++,当他的左子树节点增加,该节点的平衡因子就--。

于是我们就会遇见以下三种情况:

1、平衡因子更新为0:

这说明之前的平衡因子为-1或者1,都有过高度差值,但此次插入导致差值为0,两边子树高度相同。所以不需要再继续向父节点检查。

2、平衡因子更新为-1或者1:

这说明之前的平衡因子为0(不可能为-2或者2,因为说明插入前就不是AVL树了),此次插入将之前平衡的高度差再次拉上差距,我们需要继续向上检查当前节点的父节点,是否会出现平衡因子异常。并且,若该节点为父节点的左子节点,就让父节点平衡因子--,否则让其++。

3、平衡因子为-2或者2:

这后面插入之后已经不是一个AVL树了,我们需要对该异常节点进行旋转操作。

二、AVL树的旋转

1、左单旋:

以这个抽象图为例,Parent的左子树高度为h,我们命名为a,subR为Parent的右子树,subR左右子树的高度一开始都是h,我们分别命名为b,c。

如果要对Parent进行左旋,那么此时a,b,c的高度都必须为h,并且c子树必须为满二叉树(如果不是,那么插入到空缺的叶子结点,高度不变,高度差仍然为1),否则Parent节点不能满足两边子树高度差绝对值大于1的条件。

此时只要在c子树上插入任意一个结点,都会使c的高度变为h+1,导致subR的高度差为1,根据上文,这会导致继续向上检查父节点,(父节点原本平衡因子为1),更新后为2,由于两个节点的平衡因子分别为2,1,同号单旋,异号双旋,所以需要对Parent节点进行左单旋。

具体方法就是将subR的左子树赋给Parent的右子树,让Parent的父节点指向subR节点。

我们以一个比较理解的例子为例:

在这个例子中,h为0,我们插入一个C节点到B节点的右子树,就会导致A节点的平衡因子出现问题,需要进行左旋操作。

随后我们将b的左子树给A的右子树(因为这里B的左子树为空,所以就没体现出来),随后让A的父节点变为B的父节点(在这里要判断A为他父节点的什么子树,左还是右,随后给B相应的身份)。

最后不要忘记更新平衡因子为0.

代码实现:

(我们以这样的定义为背景(后面的代码一样))

template<class T, class K>
struct AVLTreeNode
{
	AVLTreeNode(const pair<T, K>& kv)
		:_kv(kv)
		, parent(nullptr)
		, left(nullptr)
		, right(nullptr)
		, _bf(0)
	{}
	AVLTreeNode<T, K>* right;
	AVLTreeNode<T, K>* left;
	AVLTreeNode<T, K>* parent;
	pair<T, K> _kv;
	int _bf;
};

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

parent指向父节点,_bf为平衡因子,_kv为存储的数据

本文为旋转的介绍,所以AVL树的删除插入一律跳过,想看的朋友可以在评论区留言,我也许会单独发一篇AVL树的模拟实现讲解。

void RotateL(Node* Parent)
{
	Node* pParnet = Parent->parent;//指向Parent的父节点
	Node* subR = Parent->right;//subR节点
	Node* subRL = subR->left;//subRL节点,指向等会交给Parent的右子树的节点

	//先把subRL给Parent的右子树
	Parent->right = subRL;
	if (subRL)//判断subRL是否为空
	{
		//不为空时要更新subRL的父节点
		subRL->parent = Parent;
	}

	//随后判断Parent的父节点是否为空,为空就说明Parent为当前树的根节点。
	if (pParnet == nullptr)
	{
		_root = subR;
		subR->parent = nullptr;//进行更新,根节点替换为subR
	}
	else
	{
		//不为空
		if (pParnet->left = Parent)
		{
			//Parent为pParent的左子树节点
			pParnet->left = subR;
		}
		else
		{
			ppParent->right = subR;
		}
		subR->parent = pParnet;//更新subR的父节点
	}
	//旋转结束后,更新平衡因子
	Parent->_bf = subR->_bf = 0;//进行左旋的条件是,Parent的平衡因子一开始为2,subR平衡因子为1,二者同号且为正,进行左单旋
}

按照一开始讲解的逻辑按部就班的书写代码就行,注意的是一开始传递的参数只有一个Parent节点,我们需要提前创建指针指向subR,subRL,pParent。

2、右单旋

与左单旋相对应的就是右单旋,他就像是左单旋的轴对称一样。

此时只要对a进行插入(a必须为满二叉树),就会触发连续向上的平衡因子更新检查,一直更新到subL为-1,随后向上导致Parent平衡因子为-2 。

同号单旋,异号双旋,由于都是负数,就对Parent进行右单旋。

一样的逻辑,先让Parent的左子树指向subL的右子树节点,随后让Parent的父节点成为subL的父节点。

代码演示:

	void RotateR(Node* Parent)
	{
		Node* pParent = Parent->parent;
		Node* subL = Parent->left;
		Node* subLR = subL->right;

		Parent->left = subLR;
		if (subLR)//subLR是否为空
		{
			subLR->parent = Parent;
		}

		if (pParent == nullptr)//pParent是否为空
		{
			_root = subL;
			subL->parent = nullptr;
		}
		else
		{
			if (pParent->left = Parent)
			{
				pParent->left = subL;
			}
			else
			{
				pParent->right = subL;
			}

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

3、右左双旋

我们之前只分析了二者平衡因子同号的情况,倘若Parent平衡因子为2,子树平衡因子为-1,或者Parent平衡因子为-2,子树平衡因子为1的时候,又该怎么办呢?

我们发现倘若在进行单项旋转的话,avl树仍然不会保持平衡。这时候就就需要进行双旋了。

由于Parent为2,子树为-1,异号进行左右双旋,先对子树进行左旋,再对Parent进行右旋。

注意,此时要更新旋转后的平衡因子,结果与subRL的平衡因子有关系,倘若subRL为-1,说明Parent的最后平衡因子为0,两个子树高度都为h,而subR的平衡因子为1,因为右子树高度为h,左子树高度为h-1。

代码演示如下:

void RotateRL(Node* Parent)
{
	Node* subR = Parent->right;
	Node* subRL = subR->left;
	int bf = subRL->_bf;//此时的subRL不可能为空,因为a的高度为h,bc高度为h-1,d高度为h,要想旋转,subRL必须存在。
	//或者说,subRL至少也是那个新插入的节点即:h为0,h-1代表subRL就是新插入的节点

	//我们这里可以复用之前写的单旋代码:
	RotateR(subR);
	RotateL(Parent);

	if (bf == 0)
	{
		//说明subRL就是新插入的节点
		subR->_bf = subRL->_bf = Parent->_bf = 0;
	}
	else if (bf == -1)
	{
		//插入在subRL的左树上
		Parent->_bf = subRL->_bf = 0;
		subR->_bf = 1;
	}
	else if (bf == 1)
	{
		Parent->_bf = -1;
		subR->_bf = subRL->_bf = 0;
	}
	else
	{
		assert(false);//说明出现了其他情况,报错就行了
	}
}

注意,在复用之前单旋代码前,我们必须先保存当前subR,subRL的节点指针,并且知道subRL的平衡因子大小,这对我们更新最后的平衡因子有帮助。

4、左右双旋

如图,左右双旋也是右左双旋的翻版,思路只能说是差不了多少。

	void RotateLR(Node* Parent)
	{
		Node* subL = Parent->left;
		Node* subLR = subL->right;
		int bf = subLR->_bf;

		RotateL(subL);
		RotateR(Parent);

		if (bf == 0)
		{
			subL->_bf = subLR->_bf = Parent->_bf = 0;
		}
		else if (bf == 1)
		{
			Parent->_bf = -1;
			subL->_bf = subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			subL->_bf = 1;
			subLR->_bf = Parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

 总结:

AVL树作为自平衡二叉搜索树的经典实现,通过对树的高度进行严格控制,确保了高效的查找、插入和删除操作。尽管其操作复杂度较高,但在需要频繁查找和维护较大数据集的场景中,AVL树无疑是一种值得选择的数据结构。

 优点

  • 平衡性好:通过自动调整树的高度,确保在最坏情况下,操作的时间复杂度保持在O(log n)。
  • 查找性能稳定:在大量数据插入或删除操作后,AVL树能够依然保持较好的查找性能。

缺点

  • 插入与删除操作复杂度较高:每次插入或删除节点后,可能需要进行多次旋转操作来恢复平衡,增加了操作的复杂性和耗时。
  • 空间开销大:为了维护平衡因子,AVL树需要在每个节点存储额外的高度信息,增加了空间开销。
  • 25
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

渡我白衣

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

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

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

打赏作者

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

抵扣说明:

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

余额充值