什么是平衡二叉树
利用二叉查找树,我们在搜索某个值的时候,效率会得到巨大提升。但是虽然看起来比较完美,也是存在缺陷的,比如现在我们依次将下面的值插入到这棵二叉树中:
1 | 20 15 13 8 6 3 |
在插入完成后,我们会发现这棵二叉树竟然长这样:

其说它是一棵二叉树,不如说就是一个链表,如果这时我们想要查找某个结点,那么实际上查找的时间并没有得到任何优化,直接就退化成线性查找了。
所以,二叉查找树只有在理想情况下,查找效率才是最高的,而像这种极端情况,就性能而言几乎没有任何的提升。我们理想情况下,这样的效率是最高的:

所以,我们在进行结点插入时,需要尽可能地避免这种一边倒的情况,这里就需要引入平衡二叉树的概念了。实际上我们发现,在插入时如果不去维护二叉树的平衡,某一边只会无限制地延伸下去,出现极度不平衡的情况,而我们理想中的二叉查找树左右是尽可能保持平衡的,平衡二叉树(AVL树)就是为了解决这样的问题而生的。
它的性质如下:
- 平衡二叉树一定是一棵二叉查找树。
- 任意结点的左右子树也是一棵平衡二叉树。
- 从根节点开始,左右子树都高度差不能超过1,否则视为不平衡。
一个节点的高度 = 从该节点到它所有叶子节点中,最长那条路径的边数(或节点数)
我们接下来讲解的将会以包括自身的节点数来总计高度
可以看到,这些性质规定了平衡二叉树需要保持高度平衡,这样我们的查找效率才不会因为数据的插入而出现降低的情况。二叉树上节点的左子树高度 减去 右子树高度, 得到的结果称为该节点的平衡因子(Balance Factor),比如:

通过计算平衡因子,我们就可以快速得到是否出现失衡的情况。比如下面的这棵二叉树,正在执行插入操作:

可以看到,当插入之后,不再满足平衡二叉树的定义时,就出现了失衡的情况,而对于这种失衡情况,为了继续保持平衡状态,我们就需要进行处理了。我们可能会遇到以下几种情况导致失衡:

如果看不懂的话,点击我可以查看动画效果
LL形调整(右旋)

对于LL型失衡,我们只需要进行右旋操作,首先我们先找到最小不平衡子树,注意是最小的那一个:

可以看到根结点的平衡因子是2,是目前最小的出现不平衡的点,所以说从根结点开始向左的三个结点需要进行右旋操作,右旋需要将这三个结点中间的结点作为新的根结点,而其他两个结点现在变成左右子树:

RR型调整(左旋)
前面我们介绍了LL型以及右旋解决方案,相反的,当遇到RR型时,我们只需要进行左旋操作即可:

操作和上面是一样的,只不过现在反过来了而已:

RL型调整(先右旋,再左旋)
剩下两种类型比较麻烦,需要旋转两次才行。我们来看看RL型长啥样:

可以看到现在的形状是一个回旋镖形状的,先右后左的一个状态,也就是RL型,针对于这种情况,我们需要先进行右旋操作,注意这里的右旋操作针对的是后两个结点:

其中右旋和左旋的操作,与之前一样,该怎么分配左右子树就怎么分配,完成两次旋转后,可以看到二叉树重新变回了平衡状态。
LR型调整(先左旋,再右旋)
和上面一样,我们来看看LR型长啥样,其实就是反着的:

形状是先向左再向右,这就是典型的LR型了,我们同样需要对其进行两次旋转:

代码实现
这样,我们只需要在插入结点时注意维护整棵树的平衡因子,保证其处于稳定状态,这样就可以让这棵树一直处于高度平衡的状态,不会再退化了。这里我们就编写一个插入结点代码来实现一下吧,首先还是结点定义:
1 | typedef int E; |
接着我们需要先将左旋、右旋等操作编写出来,因为一会插入时可能需要用到:
1 | int max(int a, int b){ |
最后就是我们的插入操作了,注意在插入时动态计算树的高度,一旦发现不平衡,那么就立即采取对应措施:
1 | Node insert(Node root, E element){ |
这样,我们就完成了平衡二叉树的插入操作,当然删除操作比较类似,也是需要在删除之后判断是否平衡,如果不平衡同样需要进行旋转操作,这里就不做演示了。
说些什么吧!