文章目录
前言
接上面一篇文章:
【技术点】数据结构–二叉树(一)
这次讲点更厉害的树:平衡二叉树(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型旋转:
我们可以看出,是以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是指某个节点有右子树再增加左子树造成的不平衡:
此时,我们可以把这个旋转两次,做一次LL旋转,然后再做一次RR旋转就可以。
当然,这里要多做一点处理,把空节点也画出来,就很能说明问题了:
- 在第一次旋转时,不平衡节点为key=3,所以根据LL旋转的规则就会变成中间的样子
- 第二次旋转时,不平衡节点成为了key=1,所以再做一次RR就变成了最终的图。
如何确定旋转类型
好了,通过几种情况的旋转,就可以得到一颗新的平衡二叉树。
但问题在于,新增一个节点时,如何确认是哪种旋转呢?
- 确定不平衡节点(node),也就是说某个节点的平衡因子大于1了。
- 根据新节点的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,所以这里应该还有其他处理步骤,懒得写了,也就是一些指针的调整了
}
}
}
}
下一步
本来想这里连红黑树一起写了的,结果发现一下子写了这么多,红黑树放下一篇吧,敬请期待:
数据结构–二叉树(三)