数据结构 | 第7章 - AVL树

243 阅读9分钟

7.4 AVL树

在渐进意义下,AVL树可始终将其高度控制在O(logn)以内,从而保证每次查找、插入或删除操作,均可在O(logn)的时间内完成。

定义与性质

  • 平衡因子

    任一节点v的平衡因子(balance factor)定义为“其左、右子树的高度差”,即

    balFac(v) = height(lc(v)) - height(rc(v))

    所谓AVL树,即平衡因子受限的二叉搜索树——其中各节点平衡因子的绝对值均不超过1。

  • 接口定义

    基于BST定义的AVL树接口:

     0001 #include "BST/BST.h" //基于BST实现AVL树
     0002 template <typename T> class AVL : public BST<T> { //由BST派生AVL树模板类
     0003 public:
     0004    BinNodePosi<T> insert ( const T& e ); //插入(重写)
     0005    bool remove ( const T& e ); //删除(重写)
     0006 // BST::search()等其余接口可直接沿用
     0007 };
    

    用于简化AVL树算法描述的宏:

     0001 #define Balanced(x) ( stature( (x).lc ) == stature( (x).rc ) ) //理想平衡条件
     0002 #define BalFac(x) ( stature( (x).lc ) - stature( (x).rc ) ) //平衡因子
     0003 #define AvlBalanced(x) ( ( -2 < BalFac(x) ) && ( BalFac(x) < 2 ) ) //AVL平衡条件
    
  • 平衡性

    就渐进意义而言,AVL树的确是平衡的。

  • 失衡与重平衡

    image-20220719164452014.png

    因节点x的插入或删除而暂时失衡的节点,构成失衡节点集,记作UT(x)。请注意,若x为被摘除的节点,则UT(x)仅含单个节点;但若x为被引入的节点,则UT(x)可能包含多个节点。由以上实例,也可验证这一性质。

节点插入

  • 失衡节点集

    新引入节点x后,UT(x)中的节点都是x的祖先,且高度不低于x的祖父。以下,将其中的最深者记作g(x)。在x与g(x)之间的通路上,设p为g(x)的孩子,v为p的孩子。注意,既然g(x)不低于x的祖父,则p必是x的真祖先。

  • 重平衡

    首先,需要找到如上定义的g(x),可从x出发沿parent指针逐层上行并核对平衡因子,首次遇到的失衡祖先为g(x),这一过程只需O(logn)时间。

     0001 /******************************************************************************************
     0002  * 在左、右孩子中取更高者
     0003  * 在AVL平衡调整前,借此确定重构方案
     0004  ******************************************************************************************/
     0005 #define tallerChild(x) ( \
     0006    stature( (x)->lc ) > stature( (x)->rc ) ? (x)->lc : ( /*左高*/ \
     0007    stature( (x)->lc ) < stature( (x)->rc ) ? (x)->rc : ( /*右高*/ \
     0008    IsLChild( * (x) ) ? (x)->lc : (x)->rc /*等高:与父亲x同侧者(zIg-zIg或zAg-zAg)优先*/ \
     0009    ) \
     0010    ) \
     0011 )
    

    这里并未显式地维护各节点的平衡因子,而是在需要时通过比较子树的高度直接计算。

    以下,根据节点g(x)、p和v之间具体的联接方向,将采用不同的局部调整方案,分述如下:

  • 单旋

    v是p的右孩子,且p是g的右孩子:

    image-20220719171140574.png

    该操作为zag(g(x))

    不难验证,通过zig(g(x))可以处理对称的失衡。

  • 双旋

    节点v是p的左孩子,而p是g(x)的右孩子:

    image-20220719171430312.png

    该操作为zig(p)和zag(g(x))

    不难验证,通过zag(p)和zig(g(x))可以处理对称的情况。

  • 高度复原

    换而言之,在AVL树中插入新节点后,仅需不超过两次旋转,即可使整树恢复平衡。

  • 实现

       0001 template <typename T> BinNodePosi<T> AVL<T>::insert ( const T& e ) { //将关键码e插入AVL树中
       0002    BinNodePosi<T> & x = search ( e ); if ( x ) return x; //确认目标节点不存在
       0003    BinNodePosi<T> xx = x = new BinNode<T> ( e, _hot ); _size++; //创建新节点x
       0004 // 此时,x的父亲_hot若增高,则其祖父有可能失衡
       0005    for ( BinNodePosi<T> g = _hot; g; g = g->parent ) //从x之父出发向上,逐层检查各代祖先g
       0006       if ( !AvlBalanced ( *g ) ) { //一旦发现g失衡,则(采用“3 + 4”算法)使之复衡,并将子树
       0007          FromParentTo ( *g ) = rotateAt ( tallerChild ( tallerChild ( g ) ) ); //重新接入原树
       0008          break; //局部子树复衡后,高度必然复原;其祖先亦必如此,故调整结束
       0009       } else //否则(g仍平衡)
       0010          updateHeight ( g ); //只需更新其高度(注意:即便g未失衡,高度亦可能增加)
       0011    return xx; //返回新节点位置
       0012 } //无论e是否存在于原树中,总有AVL::insert(e)->data == e
    
  • 效率

    AVL树的节点插入操作可以在O(logn)时间内完成。

节点删除

  • 失衡节点集

    与插入操作十分不同,在摘除节点x后,以及随后的调整过程中,失衡节点集UT(x)始终至多只含一个节点。而且若该节点g(x)存在,其高度必与失衡前相同。

    另外还有一点重要的差异是,g(x)有可能就是x的父亲。

  • 重平衡

    与插入操作同理,从_hot节点(7.2.6节)出发沿parent指针上行,经过O(logn)时间即 可确定g(x)位置。作为失衡节点的g(x),在不包含x的一侧,必有一个非空孩子p,且p的高度至少为1。于是,可按以下规则从p的两个孩子(其一可能为空)中选出节点v:若两个孩子不等高,则v取作其中的更高者;否则,优先取v与p同向者(亦即,v与p同为左孩子,或者同为右孩子)。

    以下不妨假定失衡后g(x)的平衡因子为+2(为-2的情况完全对称)。根据祖孙三代节点g(x)、p和v的位置关系,通过以g(x)和p为轴的适当旋转,同样可以使得这一局部恢复平衡。

  • 单旋

    image-20220720164855748.png

  • 双旋

    image-20220720164945566.png

  • 失衡传播

    与插入操作不同,在删除节点之后,尽管也可通过单旋或双旋调整使局部子树恢复平衡,但就全局而言,依然可能再次失衡。

    在摘除节点之后的调整过程中,这种由于低层失衡节点的重平衡而致使其高层祖先失衡的现象,称作“失衡传播”

  • 实现

     0001 template <typename T> bool AVL<T>::remove ( const T& e ) { //从AVL树中删除关键码e
     0002    BinNodePosi<T> & x = search ( e ); if ( !x ) return false; //确认目标存在(留意_hot的设置)
     0003    removeAt ( x, _hot ); _size--; //先按BST规则删除之(此后,原节点之父_hot及其祖先均可能失衡)
     0004    for ( BinNodePosi<T> g = _hot; g; g = g->parent ) { //从_hot出发向上,逐层检查各代祖先g
     0005       if ( !AvlBalanced ( *g ) ) //一旦发现g失衡,则(采用“3 + 4”算法)使之复衡,并将该子树联至
     0006          g = FromParentTo ( *g ) = rotateAt ( tallerChild ( tallerChild ( g ) ) ); //原父亲
     0007       updateHeight ( g ); //更新高度(注意:即便g未失衡或已恢复平衡,高度均可能降低)
     0008    } //可能需做Omega(logn)次调整——无论是否做过调整,全树高度均可能降低
     0009    return true; //删除成功
     0010 }
    
  • 效率

    综合各方面的消耗,AVL树的节点删除操作总体的时间复杂度依然是O(logn)。

统一重平衡算法 —— “3 + 4”重构

上述重平衡的方法,需要根据失衡节点及其孩子节点、孙子节点的相对位置关系,分别做单旋或双旋调整。按照这一思路直接实现调整算法,代码量大且流程繁杂,必然导致调试困难且容易出错。为此,本节将引入一种更为简明的统一处理方法。

无论对于插入或删除操作,新方法也同样需要从刚发生修改的位置x出发逆行而上,直至遇到最低的失衡节点g(x)。于是在g(x)更高一侧的子树内,其孩子节点p和孙子节点v必然存在,而且这一局部必然可以g(x)、p和v为界,分解为四棵子树——T0至T3

如下图所示,将这三个节点与四颗子树重新“组装”起来,恰好即是一颗AVL树!

image-20220720180843479.png

代码:“3 + 4”重构:

 0001 /******************************************************************************************
 0002  * 按照“3 + 4”结构联接3个节点及其四棵子树,返回重组之后的局部子树根节点位置(即b)
 0003  * 子树根节点与上层节点之间的双向联接,均须由上层调用者完成
 0004  * 可用于AVL和RedBlack的局部平衡调整
 0005  ******************************************************************************************/
 0006 template <typename T> BinNodePosi<T> BST<T>::connect34 (
 0007    BinNodePosi<T> a, BinNodePosi<T> b, BinNodePosi<T> c,
 0008    BinNodePosi<T> T0, BinNodePosi<T> T1, BinNodePosi<T> T2, BinNodePosi<T> T3
 0009 ) {
 0010    a->lc = T0; if ( T0 ) T0->parent = a;
 0011    a->rc = T1; if ( T1 ) T1->parent = a; updateHeight ( a );
 0012    c->lc = T2; if ( T2 ) T2->parent = c;
 0013    c->rc = T3; if ( T3 ) T3->parent = c; updateHeight ( c );
 0014    b->lc = a; a->parent = b;
 0015    b->rc = c; c->parent = b; updateHeight ( b );
 0016    return b; //该子树新的根节点
 0017 }

代码:AVL树的统一重平衡:

 0001 /******************************************************************************************
 0002  * BST节点旋转变换统一算法(3节点 + 4子树),返回调整之后局部子树根节点的位置
 0003  * 注意:尽管子树根会正确指向上层节点(如果存在),但反向的联接须由上层函数完成
 0004  ******************************************************************************************/
 0005 template <typename T> BinNodePosi<T> BST<T>::rotateAt ( BinNodePosi<T> v ) { //v为非空孙辈节点
 0006    BinNodePosi<T> p = v->parent; BinNodePosi<T> g = p->parent; //视v、p和g相对位置分四种情况
 0007    if ( IsLChild ( *p ) ) /* zig */
 0008       if ( IsLChild ( *v ) ) { /* zig-zig */
 0009          p->parent = g->parent; //向上联接
 0010          return connect34 ( v, p, g, v->lc, v->rc, p->rc, g->rc );
 0011       } else { /* zig-zag */
 0012          v->parent = g->parent; //向上联接
 0013          return connect34 ( p, v, g, p->lc, v->lc, v->rc, g->rc );
 0014       }
 0015    else  /* zag */
 0016       if ( IsRChild ( *v ) ) { /* zag-zag */
 0017          p->parent = g->parent; //向上联接
 0018          return connect34 ( g, p, v, g->lc, p->lc, v->lc, v->rc );
 0019       } else { /* zag-zig */
 0020          v->parent = g->parent; //向上联接
 0021          return connect34 ( g, v, p, g->lc, v->lc, v->rc, p->rc );
 0022       }
 0023 }

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情