平衡二叉搜索树之AVL树

1,689 阅读6分钟

前言分析

二叉搜索树弊端

如果按照7、4、9、2、5、8、11的顺序添加节点

graph TD
7 --> 4 --> 2
4 --> 5
7 --> 9 --> 8
9 --> 11

但是如果按照从小到大添加节点

graph TD
2 --> 4 --> 5 --> 7 --> 8 --> 9 --> 11

很明显这样的话二叉搜索树直接退化变成了链表,这时时间复杂度将由O(logn)O(logn)退化为O(n)O(n),当n比较大的时候性能会出现较大的差异

同样,在删除节点时也会出现二叉搜索树退化变成链表的情况,所以必须要避免这种情况的发生,要让添加、删除、搜索的时间复杂度维持在O(logn)O(logn)级别

平衡(Balance)

平衡:当节点数量固定时,左右子树高度越接近,这棵二叉树就越平衡(高度越低)

如图从左至右就越来越平衡

image.png

理想平衡

最理想的平衡就是像完全二叉树满二叉树那样高度是最小的

image.png

改进二叉搜索树

  • 首先,节点的添加、删除顺序是无法限制的,可以认为是随机的
  • 所以,改进的方案应当是在节点的添加或者删除后,尽可能让二叉搜索树恢复平衡(减小树的高度)
  • 如果接着调整节点的位置,完全可以达到理想平衡,但是付出的代价会比较大(调整的次数过多,反而增加了时间复杂度)
  • 总结来说:用尽量少的调整次数达到适度平衡即可 一棵达到适度平衡的二叉搜索树可以称为:平衡二叉搜索树

平衡二叉搜索树(Balanced Binary Search Tree)

英文简称:BBST

常见的平衡二叉搜索树:

  • AVL树(Windows NT内核中广泛使用)

  • 红黑树

    • C++ STL(比如 map、set )
    • Java 的 TreeMap、TreeSet、HashMap、HashSet
    • Linux 的进程调度
    • Ngix 的 timer 管理

一般也称它们为:自平衡的二叉搜索树(Self-balancing Binary Search Tree)

AVL树

平衡因子:某节点左右子树的高度差

AVL树特点:

  • 每个节点的平衡因子只能是-10或者1
  • 每个节点左右子树高度差不超过1
  • 搜索、添加、删除的时间复杂度是O(logn)O(logn)

image.png

添加导致的失衡

示例:往下面这棵子树中添加13

◼ 最坏情况:可能会导致所有祖先节点都失衡

◼ 父节点、非祖先节点,都不可能失衡

image.png

失衡后的调整根据失衡节点和导致失衡的原因分为四种:LLLRRLRR

LL-右旋转(单旋)

image.png

在添加完红色节点后直接导致g失衡,且导致失衡的方向是g左节点的左节点,方向上是LL

修改方案:

  • g.left = p.right
  • p.right = g
  • p称为这棵子树的根节点
  • 这棵子树仍旧是一棵二叉搜索树

注意维护的内容:

  • T2pgparent属性
  • 先后更新gp的高度

RR-坐旋转(单旋)

image.png

在添加完红色节点后直接导致g失衡,且导致失衡的方向是g左节点的右节点,方向上是RR

修改方案:

  • g.right = p.left
  • p.left = g
  • p称为这棵子树的根节点
  • 这棵子树仍旧是一棵二叉搜索树

注意维护的内容:

  • T1pgparent属性
  • 先后更新gp的高度

LR-RR左旋转,LL右旋转(双旋)

image.png

先对p左旋转,再对g右旋转

RL-LL右旋转,RR左旋转

image.png

先对p右旋转再对g左旋转

代码实现

分为四种不同情况的代码实现

思路:

  1. 调整一定是要在添加之后,所以在父类二叉搜索树中定义子类实现的调整方法afterAdd

  2. 因为失衡只会发生在祖先节点上,故循环遍历增加节点的父节点、祖父节点一直向上

  3. 判断节点是否失衡

    • 如果未失衡,只需要更新节点高度

    • 如果失衡,需要恢复平衡并且退出循环,调整第一个失衡节点后,失衡子树的高度不会变化,所以向上的节点仍然平衡

image.png

image.png

image.png

恢复平衡思路:

  1. 根据上面分析,所有失衡的情况都可以归纳为四类:LLRRLRRL

  2. 根据图中找出gpn节点

    • grand节点就是失衡节点

    • parent节点就是grand的左右节点中高度最高的节点

    • 同理node节点就是parent的左右节点中高度最高的节点

  3. 其实无论是四种情况中的哪一种都可以归类为左旋或者右旋的单独操作或者结合,所以旋转方法只分为左旋右旋即可

  4. 左旋或者右旋的操作中就是维护旋转后节点之间的连线,以及最后要更新gp的高度

image.png

image.png

image.png

统一所有旋转操作

对于上面分析来说,总共分为四种情况去做了不同的操作,还是比较麻烦,现在将其归类为一种

image.png

还是同样是四种情况,但是根据顺序a<b<c<d<e<f<g,虽然是四种不同的情况,但是调整平衡之后他们的相对顺序是一样的,那么我们只需要把不同情况的abcdefg给分别找出来即可

image.png

对比这两幅图可以很快确定abcdefg的对应关系

image.png

image.png

删除导致的失衡

添加会导致失衡,同样删除也会导致失衡

例如下图删除子树中的16可能会导致父节点或者祖父节点失衡(只有一个节点失衡),其它节点都不可能失衡

image.png

同样也分四种情况来分析

LL-右旋转(单旋)

image.png

RR-左旋转(单旋)

image.png

LR – RR左旋转,LL右旋转(双旋)

image.png

RL – LL右旋转,RR左旋转(双旋)

image.png

对于上面四种情况来说,都有一些共同点:

  • 如果绿色节点不存在,更高层的祖先节点可能也会失衡,需要再次恢复平衡,然后又可能导致更高层的祖先节点失衡
  • 极端情况下,所有祖先节点都需要进行恢复平衡的操作,共 O(logn)O(logn) 次调整

删除之后的恢复平衡代码 image.png

总结

  • 添加

    • 可能会导致所有祖先节点都失衡
    • 只要让高度最低的失衡节点恢复平衡,整棵树就恢复平衡(仅需O(1)O(1)级别的调整)
  • 删除

    • 可能会导致父节点或祖先节点失衡(只有1个节点会失衡)
    • 恢复平衡后,可能会导致更高层的祖先节点失衡(最多需要 O(logn)O(logn) 次调整)
  • 平均时间复杂度

    • 搜索:O(logn)O(logn)
    • 添加:O(logn)O(logn),仅需 O(1) 次的旋转操作
    • 删除:O(logn)O(logn),最多需要O(logn)O(logn)次的旋转操作