vlambda博客
学习文章列表

算法笔记-6:平衡二叉树(理论篇)

        上期我们说到,如果按从小到大的顺序向二叉搜索树插入节点,那么搜索树会退化成链表,这样就背离我们利用二叉搜索树的特性方便查询元素的初衷了。

        有没有办法让这个比萨斜塔一样的结构变成一棵左右平衡的二叉搜索树呢?

图1-退化成链表的二叉搜索树


        不如,把链表打个对折?

算法笔记-6:平衡二叉树(理论篇)

图2-链表对折


        感觉像搭了个人字梯,中间还是太空了……

算法笔记-6:平衡二叉树(理论篇)

        没事,注意到20左右两边分别可以视为链表,继续对折。

算法笔记-6:平衡二叉树(理论篇)

图3-继续折叠


        这种方法看起来对链表效果不错,但是对于更一般的失衡树,就很难发挥作用了,会遇到大量的子树冲突问题。

算法笔记-6:平衡二叉树(理论篇)

图4-几种非链表失衡树

        

        与其将不平衡的二叉树调整成平衡,不如一开始就构造一个平衡的二叉搜索树。每新增一个节点,就判断并重新调整平衡。这便是构造平衡二叉树的过程。

        构造平衡二叉搜索树(又称自平衡二叉搜索树)并不只有一种方法,但我们常说的平衡二叉树一般指AVL树。AVL树是最早被发明的平衡二叉树,由Adelson Velsky 和 Evgenii Landis在1962年发表的论文中提出,并以二人的名字命名。这也是我们今天将要介绍的内容。




一、平衡二叉搜索树的定义


        如果一棵二叉搜索树中,任何一个节点的左右子树高度(深度)差不超过1,那么它就是一棵平衡二叉搜索树。

算法笔记-6:平衡二叉树(理论篇)

图5-平衡二叉树

        换句话说,AVL树的平衡,是通过限制左右子树的高度差来实现的。所以AVL树又被称为高度平衡树。

        来看看如下哪些属于平衡二叉树?

算法笔记-6:平衡二叉树(理论篇)

图6

        (a)不是平衡二叉树,虽然根节点左右子树高度一样,但子节点的子树一边高度为2,一边为0。

        (b)不是平衡二叉树,虽然根节点是平衡的,右边只比左边低1层。但左子树自身仍是失衡的。

        (c)是平衡二叉树。

        (d)不是平衡二叉树。根节点失衡,左子树高为3,右子树为1。


二、平衡因子


        三四层高的树,就很容易眼花分不清平不平衡了。要是树有20多层,一层层数不得累坏去?构造平衡树,判断平不平衡的工作最终还是要交给程序来办。为了使程序能够判断二叉搜索树的平衡性,Adelson Velsky 和 Evgenii Landis在论文里提出了平衡因子的概念:

        

        节点的左子树与右子树的高度(深度)差即为该节点的平衡因子。

        

算法笔记-6:平衡二叉树(理论篇)

图7-平衡因子计算


        对于程序来说,每个节点记录以自己的高度,然后把两个子节点上报的高度相减(缺失的子节点高度设为0),就可以得到平衡因子。


        一棵空树肯定是平衡的,一棵只有根节点的树和一棵只有一个根和一个子节点的树也是平衡的。再往后,不论树中有多少个节点,一定是插入节点导致失衡,或者删除节点时导致失衡。如果我们每次插入或者删除节点后,都通过一定处理使树重新恢复平衡,那么最终一定能得到平衡二叉树。这就是构造平衡二叉树的原理。



三、插入节点导致失衡时的调整


        假如我们要在图5的节点60下面插入节点70,先来计算插入前每个节点的平衡因子,标在节点旁边:

算法笔记-6:平衡二叉树(理论篇)

图8-插入前的平衡情况

        插入节点70后,部分节点的平衡因子发生了变化:

算法笔记-6:平衡二叉树(理论篇)

图9-插入后的平衡情况

        如果沿着新插入的节点70不断地访问父节点,直到找到第一个平衡因子绝对值大于1的节点,也就是40。那么,以40这个节点为根节点的子树,就被称为最小失衡子树。

        如果把整棵树调整成平衡树太难,那么我们就先调整这棵最小失衡子树。而调整平衡,需要用到旋转方法,至于怎么旋转,则需要分四种情况讨论:


1、在失衡节点的左子节点的左子树插入节点导致失衡(LL)

        如图10,假如我们在节点5下方插入了节点2,导致节点20成为最小失衡树根节点。此时20的平衡因子是2,左子树比右子树高2,我们需要进行右旋,增加右子树高度,降低左子树高度,使平衡因子降低。

算法笔记-6:平衡二叉树(理论篇)

图10-LL右旋

        具体做法是,将20的左子节点10提升为根节点,20自身沉降为10的右子节点。此时会出现10拥有5、15、20三个子节点的情况。

        10原来的右子节点15安放在哪里呢?因为20原来的左子节点10已经变成了它的父节点,那么20现在的左子节点一定是空缺的,15刚好可以放在这个位置。

        所以右旋就是把左子节点提升为根节点,根节点沉降为右子节点,左子节点的右子树变为右子节点的左子树。为方便记忆,我们可以简称为“左升右降,左右变右左”。


2、在失衡节点的右子节点的右子树插入节点导致失衡(RR)

          如图11,假如我们在节点60下方插入了节点70,节点40的平衡因子变成-2,该子树成为最小失衡子树。

算法笔记-6:平衡二叉树(理论篇)


图11-RR左旋

        为了使其重新平衡,需要将其左旋:根节点40向左沉降,右子节点50提升为新根节点。50的原左子节点45,变成40的新右子节点。

        左旋操作可以简记为“右升左降,右左变左右”。


3、在失衡节点的左子节点的右子树插入节点导致失衡(LR)

        如图12,假如我们在节点15下方插入节点12,节点20平衡因子变为2,成为最小失衡子树的根节点。此时只进行一次右旋无法使树重新平衡,需要先左旋后右旋。

算法笔记-6:平衡二叉树(理论篇)

图12-LR先左旋后右旋

        先将20的左子节点10为根节点的子树左旋,节点15升为根节点(20的新左子节点),节点10向左沉降。12因为小于15大于10,它变成了10的右子节点。

        然后将20为根的子树右旋,15再次升为根节点,20向右沉降。由于15没有右子节点,所以20没有得到新的左子节点。

        先左旋后右旋的操作可以简记为“左子树左旋,根右旋”。


4、在失衡节点的右子节点的左子树插入节点导致失衡(RL)
        如图13,假如我们在节点45的下方插入节点48,节点40平衡因子变为-2,成为最小失衡树的根节点。只进行一次左旋无法使树重新平衡,需要先右旋后左旋。

算法笔记-6:平衡二叉树(理论篇)

图13-RL先右旋后左旋

        先将40的右子节点50为根节点的子树右旋,节点45升为根节点(40的新右子节点),节点50向右沉降。48需要转变为50的左子节点。

        然后将40为根的子树左旋,45再次升为根节点,40向左沉降。由于45没有左子节点,40没有获得新的右子节点。

        先右旋后左旋的操作可以简记为“右子树右旋,根左旋”。



四、四种插入情况的平衡因子分析


        这四种插入情况,如何让程序判断呢?

        仔细观察最小失衡子树中平衡因子的变化。首先,插入节点导致失衡,必然是因为失衡树的其中一个子树树高发生了变化。从左子树中插入,必然是提高了左子树高度,那么根节点平衡因子一定为2;如果是在右子树中插入,平衡因子一定为-2。

算法笔记-6:平衡二叉树(理论篇)

图14-失衡节点平衡因子比较

        平衡因子为2的情况下,考察左子节点的平衡因子,如果是1,说明它的左子树高于右子树,高的原因是其左子树插入了节点,这种情况是LL。

        如果是-1,一定是其右子树插入了节点,这种情况是LR。

算法笔记-6:平衡二叉树(理论篇)

图15-LL和LR左子节点平衡因子比较

        同样地,平衡因子为-2的情况下,就考察右子节点的平衡因子,如果是1,说明是RL情况;如果是-1,说明是RR情况。

算法笔记-6:平衡二叉树(理论篇)

图16-RL和RR右子节点平衡因子比较


        总结四种插入的情况和相应的旋转方法,如下表:

算法笔记-6:平衡二叉树(理论篇)

图17-四种插入情况总结



五、删除节点导致失衡时的调整


        在上期算法笔记中,我们讨论了二叉搜索树的删除一共分三种情况:

        1、删除的节点为叶子节点

        2、删除的节点只有一个子节点

        3、删除的节点有两个子节点


        如果严格按照上期中所述流程操作,AVL树删除节点后至少可以成为一棵二叉搜索树。接下来只需要保证是否所有节点满足平衡树的要求,即平衡因子绝对值小于2就行了。

        那么,是否需要把每个节点的平衡因子遍历一遍呢?


1、删除节点后的平衡性检查

        考察下图删除节点35和50的过程。删除节点35后,其父节点没有了左子树,右子树高仍为1,因此平衡因子变成了-1。但40所处的子树高度没有变化,因此40的父节点30平衡因子不变。

算法笔记-6:平衡二叉树(理论篇)

图18-删除节点35和50

        继续删除节点50,40的平衡因子变为了0。而因为40所处的子树高变成了1,其父节点30的平衡因子变为2,成为最小失衡节点。


        从上图的例子中我们可以看出,删除会使被删除节点所属的子树高度变化,进而影响到其父节点,乃至父节点的父节点平衡因子变化。但是,并不影响其兄弟节点和父节点的兄弟节点。因此,我们只需要从被删除节点的位置不断向上访问父节点。当所有父节点平衡因子绝对值小于2时,整棵树就仍旧是平衡的。

        而当删除导致失衡时,第一个访问到平衡因子为2或者-2的节点即是最小失衡节点。然后,我们可以判断其失衡类型来选择将其恢复平衡的旋转方式。因为我们已经分析出了LL,LR,RL,RR四种失衡类型其失衡节点和子节点平衡因子的关系,所以虽然这次不是因为插入节点导致的失衡,我们仍旧可以根据节点30平衡因子为2和节点20平衡因子为-1推断出其属于LR失衡类型,需要进行先左旋,后右旋的操作。

算法笔记-6:平衡二叉树(理论篇)

图19-删除50后的旋转调整

        仔细观察旋转过程,我们可以发现25成为了新的(子)树根。而25原来的两个子节点:左子节点22在左旋(先)过程中成为了20的右子节点,而28在右旋(后)过程中成为了30的左子节点。


2、插入节点中未出现过的失衡情况

        需要注意的是,假如节点10还有子节点5和15,此时节点20的平衡因子为0。这种失衡节点平衡因子为2,其子节点却为0的情况,是我们在讨论插入节点引起的失衡时从未遇到过的,这时该怎么办呢?

        简单尝试后就会发现,这种情况一次右旋即可达到平衡,因此它可以归入LL类型。

算法笔记-6:平衡二叉树(理论篇)

图20-一次右旋即可平衡


        根据这个新发现,我们可以更新图17的表格:

算法笔记-6:平衡二叉树(理论篇)

图21-失衡类型及处理


3、发生节点替代后的平衡调整

        另一个需要注意的点是,删除叶子节点和只有一个子节点的节点。都可以从被删除的节点开始向上搜索父节点,访问平衡因子的情况。但是删除有两个子节点的节点,会涉及到寻找替代节点的问题。找到替代节点后,需要从替代节点的原位置开始向上搜索父节点。


        如下图,删除节点40后,找到替代节点45。而替代节点换上去之后,引起了节点50的失衡。

图22-删除节点40

        可以根据平衡因子推断出节点50的失衡类型为RR失衡,一次左旋即可恢复平衡,如下图:

图23-一次左旋即可平衡

        接着检查50的父节点60、45、30的平衡因子,都是0,那么整棵树恢复平衡完毕。


        综上所述,AVL删除节点的流程可以归纳为:

        1、按二叉搜索树的流程删除节点

        2、从被删除节点或替代节点的位置向上搜索,直到搜索到最小失衡节点或者根节点

        3、判断失衡类型,执行旋转操作

        4、从原最小失衡节点继续向上搜索


        由于AVL树的篇幅较长,这次只介绍AVL树的理论知识。编写代码的内容,我们放在下一期中介绍。这里抛出一个问题供大家思考:为何LR和RL失衡类型无法通过一次旋转达到平衡?大家可以试着旋转操作看看,把答案打在