查找树(二):AVL树

1. AVL树

前面的探讨已经可以知道,普通的二叉查找树的查找操作并不能实现O(log2n)的时间复杂度,因此需要为它添加平衡条件。这包含两个层面的意思:

  • 添加了平衡条件后,树的深度必须是O(log2(N)),毕竟这就是我们添加平衡条件的初衷;
  • 平衡条件必须要容易保持。举个例子,说起平衡条件,最容易想到的就是让每个节点的的左右子树的高度都保持一致,虽然这种平衡条件保证了树的深度,但它“太严格了”,因而难以被使用(首先它需要节点数为2^k-1,其次树的构造和保持也是一难点)。

正如上面所说,严格限制每个节点的左右子树高度相同是不太现实的,我们需要略微放宽条件。下面我们来看AVL树的定义(关于树的深度/高度的定义可查看:):

  • 首先它是一棵二叉查找树;
  • 其次AVL树每个节点的左子树和右子树的高度最多差1(空树的高度定义为-1)。

可以证明,粗略地说,一个AVL树的高度最多为1.44*log2(N+2)-1.328,但是实际上的高度只略大于log2(N)。根据AVL树的定义,现在我们已经可以准确的判断出一颗二叉查找树是否为AVL树了,不妨看下面的例子:
AVL树与非AVL树
很显然,左边的是一棵AVL树,而右边的并不是,他的根节点就不满足左右子树高度差为1的条件(其根节点左右子树的高度差为2,左子树高度为2,右子树高度为0)。

清楚了AVL树的定义之后,下一步当然就是实现它。乍一看AVL树的定义,可能会觉得很抽象,不妨将其实现进行分解,这里我们主要关注AVL树的查找、插入(构造一棵AVL树的过程就是不断插入新的节点的过程)、删除操作

  • 对于查找操作,因为它是一棵二叉查找树,因此可以参考二叉查找树的查找操作,AVL树所作的只是对查找操作的时间复杂度做了优化,达到了O(log2(N))。
  • 对于插入操作,只需要保证插入了一个新的节点之后,AVL树的平衡条件还可以成立,这一点可以通过旋转操作实现。
  • 删除操作也会破坏AVL树的平衡条件,他与插入操作类似,也可以通过旋转操作恢复,只是具体的操作有些许差别。

2. 插入操作

执行插人操作后,只有那些从插入点到根节点的路径上的节点的平衡可能被破坏,因为只有这些节点的子树可能发生变化。我们沿着这条路径,从插入点向根节点更新平衡信息,就可以发现一个节点(如果存在的话),它的新平衡破坏了AVL条件,将其命名为A节点。

由于插入之前树是平衡的,因此A节点的左右子树的高度差为2,可以将其分为四种情况:

  1. 对A的左儿子的左子树进行一次插入;
  2. 对A的左儿子的右子树进行一次插入;
  3. 对A的右儿子的左子树进行一次插人;
  4. 对A的右儿子的右子树进行一次插入。

仔细观察,可以发现上述四种情况中,1和4、2和3相当于是镜像的操作,因此可以将四种情况简化为两种:

  • 插入操作发生在同一边(左儿子-左儿子的左子树、右儿子-右儿子的左子树);
  • 插入操作发生在不同边

对于同一边的情况,只需要单旋转(只需要旋转一次)就可以恢复平衡;而对于在不同边的情况,则需要双旋转来恢复平衡。

2.1 单旋转

对于插入操作位于同一边的操作,只需要通过一次旋转操作就可以恢复节点的平衡条件。以左-左为例(对A的左儿子的左子树进行一次插入,破坏了平衡条件):
左-左破坏了平衡
节点A不满足AVL平衡条件,它的左子树的高度比右子树大2。为了恢复树的平衡,我们需要把X上移一层,并把Z下移一层,我们将此操作成为旋转。 如下图所示:
单旋转
首先,可以证明经过了单次旋转之后,整棵子树达还是二叉查找树:

  • 旋转之前(左图),Y位于A节点的“左边”,即Y中节点的key均小于节点A的key;旋转之后(右图),由于Y小于A,因此可以作为A的左子树,符合二叉查找树的规则。
  • 旋转之前,AL是A的左节点,即AL小于A;旋转之后,A是AL的右节点,符合二叉查找树的规则。
  • 子树A、Z的“左右位置“与原来相同,并没有破坏二叉查找树的规则。

其次,可以证明经过了单次旋转之后,整棵子树符合AVL平衡条件:

  1. 观察旋转之前(左图)的结构,从前面的分析已经可以知道左子树的高度比右子树Z的高度大2,只有这样才可以破坏A的平衡;

  2. 由于在X中插入节点之前,树是平衡的,因此A的左子树的高度必定增加了,高度的增加必定是由X引起的,因此,插入后X的高度应该大于Y的高度(否则A的左子树高度不可能增加);

  3. 由于A是从插入点向根节点搜索得到的第一个不平衡点,因此AL是平衡的,因此X和Y的高度差最多差1,结合第二点的X高度大于Y,可得X的高度比Y大1;

  4. 做一个简单的归纳,定义”水平“为子树的根节点到最深的节点的高度差,L(X)代表X的水平,也就是X的高度加1(AL节点),那么可以有:

    L(X) = L(Y) + 1 = L(Z) + 2

  5. 观察旋转之后(右图)的结构,旋转操作让X、Y、Z的水平发生了变化:

    L(X) = L(X) - 1
    L(Y) = L(Y)
    L(Z) = L(Z) + 1

  6. 对比5和6的层次关系,可以知道旋转之后X、Y、Z均处于同一水平,子树的平衡得到恢复。

对于右-右的情况,可以按照类似的旋转操作进行重新平衡;而对于第二类情况(不同边),则需要双旋转,才可以恢复子树的平衡。

2.2 双旋转

不同边的情况可以用下图进行表示(以左-右为例):
左-右
此时,Y成为了破坏A的平衡的罪魁祸首,根据上面的分析可知,单旋转前后Y的层次不变,虽然Z的层次增加了,但X的层次减少了,X与Y形成了新的不平衡条件,因此单旋转并不能解决这种情况,这时候就需要双旋转来恢复平衡条件。

所谓的双旋转,就是指先进行一次单旋转,将子树转化成类似同一边的情况;然后再进行第二次单旋转以恢复整棵子树的平衡。 为方面演示,将子树Y进行展开,双旋转中第一次旋转的流程如下所示:
双旋转的第一次旋转
第一次旋转,相当于以A的左孩子AL为目标节点,进行了一次”右-右“情况的单旋转。可以看到,第一次旋转过后,已经基本等同于2.1中的情况(可能略微有所区别,但不影响结果),因此,只需要像2.1中一样在进行一次单旋转即可恢复平衡。

  • 旋转前(左图),AL右子树的层次比Z的层次大2,也就是说x、y中必然有一个是比z的层次大2的(不必关系是哪一个,并没有影响);
  • 第一次旋转后,w层次加1(比z大2)、x的不变(比z大1或2)、y的减1(比z大1或和z相等),此时就转化成了ARL的左子树层次比z大2;
  • 第一次旋转后y的层次可能比ALR的左子树小2,破坏了平衡,但是没关系,第二次旋转之后ALR的左子树(右图中等价后的大X)层次减1,y不变,局部的平衡恢复;
  • 第二次旋转后Z的层次加1,至此,整棵子树达到平衡(第二次旋转与前面给出的左-左单旋转类似,可以尝试自己画出第二次旋转的情况);
  • 由于旋转操作并不改变平衡二叉树的性质,因此,双旋转之后,整个子树恢复成了一棵AVL树。

2.3 整棵树都平衡了吗?

再回过头去看旋转操作,我们可以发现,**无论是单旋转还是双旋转,旋转过后整棵子树不仅恢复了平衡,子树的层次(即子树的高度)也恢复到了和插入前一样(插入后整棵子树的高度增加了1,旋转后子树高度减1)。**也就是说,旋转操作后,不仅第一个失衡的节点达到了平衡,由于这个局部恢复到了插入前的深度,其上层也一定是平衡的,因此整棵树都恢复了平衡!

3. 删除操作

在AVL树中删除节点后同样可能会引发失衡,失衡后的情况与插入操作类似,只是变成了被修改(删除)的一边的高度比另一边小2(插入是修改那边子树的高度更高),此时,我们同样可以通过旋转使之恢复平衡。

一般来说,二叉查找树的删除操作会将要删除的节点首先移动到叶子节点上,然后进行删除。同样的,我们只需要从被删除的叶子节点开始,逆向往根节点进行查找,更新途中每个节点的平衡信息,就能找到不平衡的节点。然后对不平衡的点进行旋转操作,以恢复局部的平衡。唯一不同的是,删除操作的平衡不会向上传播,也就是说局部恢复平衡之后,还要继续向上查找更新,如遇到不平衡的节点则继续通过旋转操作使其平衡

以删除右子树的节点D为例,删除后A的右子树的高度比左子树小2,这时候需要找到A的左孩子AL,AL左子树的深度不小于其右子树的深度,通过单旋转就可以恢复局部平衡:
删除引起失衡
这个流程与插入操作的左-左类似,只是在上图这种情况下,旋转后子树整体的高度减1(也可能不变,当AL的左右子树高度相同时),上层节点不一定平衡。

继续以删除右子树的节点D为例,AL左子树的深度比右子树的深度小1,就需要通过双旋转来恢复平衡, 具体的操作与插入时的十分类似,具体可以参考插入操作中的双旋转示意图。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值