【技术点】数据结构--二叉树(二)

本文介绍了AVL树的概念,强调了平衡因子在保持搜索性能的重要性。详细阐述了四种旋转操作(LL型、RR型、LR型、RL型)的原理,并提供了相应的伪代码,帮助理解如何在新增节点时维护平衡二叉树的平衡状态。
摘要由CSDN通过智能技术生成

前言

接上面一篇文章:
【技术点】数据结构–二叉树(一)
这次讲点更厉害的树:平衡二叉树(AVL)

平衡二叉树

什么是平衡二叉树

平衡二叉树首先是二叉搜索树(BST)。基于BST,发现树越矮查找效率越高,进而发明了二叉平衡树。
考虑下这样一个数组数据: [10, 20, 30, 40, 50]。
如果按照顺序来构建前面提到的BST的话,最终树的形态会是这样的:
最坏情况下的二叉搜索树
那么这种情况下(最坏情况下),二叉树的搜索性能就退化成单链表了。离我们想达到的二分查找的性能差了太远。因此,需要一个指标来衡量这个搜索树的结构:
平衡因子,(BF Balance factor):BF(T)=hL-hR,其中hL和hR分别为T的左、右子树的高度。
而平衡二叉树(Balanced Binary Tree)的定义就是:空树或者任一结点左、右子树高度差的绝对值不超过1,即|BF(T)<=1|
它还有一个我们经常用到的名称:AVL树。这个得名于它的发明者G. M. Adelson-Velsky和E. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。所以称之为AVL树: Adelson-Velsky-Landis

保证平衡因子小于1

在平衡因子绝对值小于1的前提下,AVL树的搜索性能可以接近于二分查找。那么我们可以看一下要如何保证平衡因子一直小于1。

计算平衡因子

平衡因子的定义为:| hL - hR |
这里面hL和hR代表子树的深度,而子树的深度定义为所有路径的最大值。
比如下图中以key=5的节点为root节点的树的深度为4。以key=2的节点为root节点的树的深度为3。
在这里插入图片描述

//首先计算某个节点的深度
//以某一个节点为root的子树的深度定义为,所有路径的最大值
function getDeptInfo(node){
	if (node is null){
		return 0
	}
	int leftDept = getDept(node.left)
	int rightDept = getDept(node.right)
	//同时计算节点深度与平衡因子
	return (leftDept > rightDept ? leftDept : rightDept) + 1, abs(leftDept - rightDept)
}

维护平衡二叉树-新增

同样的,平衡二叉树在搜索性能上的优异,必须有其他的地方为其买单,这就是树结构的变动(新增节点和删除节点,修改可以看成是一次删除+一次新增)
构造平衡二叉树可以看成是一次新增若干个节点。
当树的节点≤2时,树总是一颗平衡二叉树。继续新增节点时,针对每一个新增节点插入,计算平衡因子。因为未插入节点时,树是一颗平衡二叉树,所以是新增的这个节点导致了平衡因子大于1了。
总共有4种情况会导致平衡因子大于1:

LL型旋转

LL是指某个节点有左子树再增加左子树造成的不平衡:
新增节点 key = 1
新增节点
LL型旋转:
LL型旋转
我们可以看出,是以key=3节点为root的这颗子树平衡因子不符合平衡二叉树的特征了,那我们可以称之为***不平衡节点***。所以,LL旋转的一般方法是:
1. 将不平衡节点的左子树作为新的根节点 root = 2
2. 不平衡节点作为新的根节点的右子树 root.right = 3
3. 原来的key=2节点的右子树成为key=3节点的左子树。

我们看一个更一般的例子,新增节点6:
新增节点
那么,在这次新增中,我们的不平衡节点是7。按照上面的一般步骤:
- 将7的左子树,也就是节点4作为新的根节点
- 将7作为新根节点的右子树,也就是4的右子树
- 将原有的4的右子树编程7的左子树

最终,我们的新树结构为:
在这里插入图片描述
还有很多种场景,就不一一画出来了。

伪代码
function ll_rotate(root){  //root为不平衡节点
	temp = root.left //将节点4先保存到临时变量
	root.left = temp.right
	temp.right = root
	root = temp
	return root
}

RR型旋转

RR旋转与LL旋转类似,是指某个节点有左子树再增加左子树造成的不平衡:
在这里插入图片描述
旋转一把:
在这里插入图片描述
和LL类似地:
1. 将不平衡节点的右子树作为新的根节点 root = 2
2. 不平衡节点作为新的根节点的左子树 root.left= 1
3. 原来的key=2节点的左子树成为key=1节点的右子树。

我们看一个更一般的例子,新增节点8:
在这里插入图片描述
那么,在这次新增中,我们的不平衡节点是3。按照上面的一般步骤:
- 将3的右子树,也就是节点5作为新的根节点
- 将3作为新根节点的左子树,也就是5的左子树
- 将原有的5的左子树变成3的右子树

最终,我们的新树结构为:
在这里插入图片描述

伪代码
function rr_rotate(root){  //root为不平衡节点
	temp = root.right//将节点4先保存到临时变量
	root.right= temp.left
	temp.left= root
	root = temp
	return root
}

LR型旋转

LR是指某个节点有左子树再增加右子树造成的不平衡:
新增节点
此时,我们可以把这个旋转两次,做一次RR旋转,然后再做一次LL旋转就可以。
当然,这里要多做一点处理,把空节点也画出来,就很能说明问题了:
旋转

  • 在第一次旋转时,不平衡节点为key=1,所以根据RR旋转的规则就会变成中间的样子
  • 第二次旋转时,不平衡节点成为了key=3,所以再做一次LL就变成了最终的图。

RL型旋转

RL是指某个节点有右子树再增加左子树造成的不平衡:
RL旋转
此时,我们可以把这个旋转两次,做一次LL旋转,然后再做一次RR旋转就可以。
当然,这里要多做一点处理,把空节点也画出来,就很能说明问题了:
两次旋转

  • 在第一次旋转时,不平衡节点为key=3,所以根据LL旋转的规则就会变成中间的样子
  • 第二次旋转时,不平衡节点成为了key=1,所以再做一次RR就变成了最终的图。

如何确定旋转类型

好了,通过几种情况的旋转,就可以得到一颗新的平衡二叉树。
但问题在于,新增一个节点时,如何确认是哪种旋转呢?

  1. 确定不平衡节点(node),也就是说某个节点的平衡因子大于1了。
  2. 根据新节点的key值来判断:
    如果 key < node.left.key,那么就是LL;
    如果 key > node.right.key,那么就是LL;
    如果 key > node.left.key,那么就是LR;
    如果 key < node.right.key,那么就是RL;

整体伪代码

function addNodeToAVL(root, newNode){
	addNode(root, newNode)   //该函数就是根据二叉树的数据分布去将新节点放到二叉树中,参考上一篇文章的内容
	foreach(node in allNode){  //新增节点后,计算所有节点的平衡因子,这有更好的算法,不需要每次都从头开始,这个不是这篇文章的内容,就按最简单的来写,最主要的是说明LL旋转
		depth = getDeptInfo(node)
		if (depth > 1){
			if (newNode.key < node.left.key){
				ll_rotate(node)
			}else if (newNode.key > node.right.key){
				rr_rotate(node)
			}else if (newNode.key > node.left.key){
				rr_rotate(node)
				ll_rotate(node)
			}else if (newNode.key < node.right.key){
				ll_rotate(node)
				rr_rotate(node)   //旋转是以不平衡节点来旋转的,不是root,所以这里应该还有其他处理步骤,懒得写了,也就是一些指针的调整了
			}
		}
	}
}

维护平衡二叉树-删除

删除也很容易了,先按照二叉树的删除逻辑删掉一个节点,再计算节点的平衡因子,根据不平衡节点的情况旋转就好了。

伪代码

function delNodeToAVL(root, delNode){
	del(root, delNode)   //该函数就是将节点从树种删除,参考上一篇文章的内容
	foreach(node in allNode){  //新增节点后,计算所有节点的平衡因子,这有更好的算法,不需要每次都从头开始,这个不是这篇文章的内容,就按最简单的来写,最主要的是说明LL旋转
		depth = getDeptInfo(node)
		if (depth > 1){
			if (newNode.key < node.left.key){
				ll_rotate(node)
			}else if (newNode.key > node.right.key){
				rr_rotate(node)
			}else if (newNode.key > node.left.key){
				rr_rotate(node)
				ll_rotate(node)
			}else if (newNode.key < node.right.key){
				ll_rotate(node)
				rr_rotate(node)   //旋转是以不平衡节点来旋转的,不是root,所以这里应该还有其他处理步骤,懒得写了,也就是一些指针的调整了
			}
		}
	}
}

下一步

本来想这里连红黑树一起写了的,结果发现一下子写了这么多,红黑树放下一篇吧,敬请期待:
数据结构–二叉树(三)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

新兴AI民工

码字不易,各位看客随意

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

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

打赏作者

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

抵扣说明:

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

余额充值