zoukankan      html  css  js  c++  java
  • AVL重平衡细节——插入

     话说这个系列鸽了好久,之前在准备语言考试,就没管博客了,现在暑假咱们继续上路!

    每当我们进行一次插入之后,整棵AVL树的平衡性就有可能发生改变,为了控制整棵树的高度,我们需要通过一系列变换(重平衡)来保证它仍满足AVL的平衡条件。我们把需要重新平衡的节点叫做 ,由于任意节点最多有两个儿子,因此高度不平衡时,的两颗子树高度差2。考虑一下产生不平衡会有几种情况,稍加思索就会明白——四种情况的插入

    1. ⍺->left->left
    2. ⍺->left->right
    3. ⍺->right->left
    4. ⍺->right->right

    情形142分别是关于的镜像对称,从理论上来讲只有两种情况,当然,从编程角度还是四种情况。

     

    先说一些约定

    struct AvlNode;
    typedef struct AvlNode *Position;
    typedef struct AvlNode *AvlTree;
    
    struct AvlNode
    {
        int value;
        AvlTree  lc;
        AvlTree  rc;
        int      Height;
    };
    
    int max(int a,int b){ return a>b?a:b;}

    下面先从思路角度予以说明,一段思路说清后立即给出代码实现,趁热打铁,就易于接受了。

     

    外侧情形-单旋转

    第一种情况是发生在外侧(左-左or右-右),情形14这需要一次单旋转来完成。

     

    先说“右-右”的情况,这种旋转也叫zag

    这里两个灰色的方块是可能插入的节点,虚线连接表示只能取一处,g是可能发生失衡的最深的节点(因为往上的祖先也有可能发生失衡),而g就是当前发生失衡最近的节点。我们要围绕着g做一次”右-右“旋转。先说一下宏观的思路:为了使树恢复平衡,我们把p的右子树整体上移一层,并把T0下移一层,不过这样一来,实际上超出了AVL的特性要求,为此我们重新安排节点以形成一颗等价的树,如下所示:

    抽象地形容就是:把树形象地看成是柔软灵活的,抓住节点p,闭上你的双眼使劲摇动它,在重力作用下p就成了新的根。BST的性质告诉我们,在原树中g<p,于是在新树中g变成了

    p的左孩子,T0和p的右子树的各自隶属关系仍然不变。子树T1包含原树中介于g和p之间的的那些节点(因为原树中g<T1<p),可以将它放在新树中g的右孩子的位置上,这样所有对元素大小的要求都能得到满足了。

    怎么做呢?首先用一个临时指针指向p

    然后我们让T1成为g的右孩子,为此要这样调整:

    接下来我们令g成为p的左孩子:

     

    接下来我们要让局部子树的根由g变化为p,然后临时指针退休。 

    如此一来就完成了“右-右”的单旋转,整理一下就能看得更清了:

     

    让我们把这个思路兑现为代码

    /*
    只有在 g 存在右孩子的情况下才被调用,在g和他的右孩子之间进行旋转操作。
    最后要记得实时更新高度,返回当前的新树根
    */
    static Position
    SingleRotateWithRight( Position g )
    {
        
        Position temp;
        
        temp = g->rc;
        g->rc = temp->lc;
        temp->lc = g;
        
        g->Height = max( Height( g->lc ), Height( g->rc ) ) + 1;
        temp->Height = max( Height( temp->rc ), g->Height ) + 1;
        
        return temp;  /* New root */
    }
    View Code

    如果在此前g以上的祖先还有发生失衡,在这个局部重平衡之后,上面的各个节点也能一并恢复平衡。因为在这里除了平衡因子外,局部子树还有一个指标:高度。留意一下我们设置的三条基准线,在插入新节点之前,原树的高度以中线为基准,对照重新平衡后的树,它的高度又回到了中间的基准线上。那这又意味着什么呢?这意义十分重大,意味着他所有祖先在计算平衡因子时所得结果,也将与插入节点前完全一样,换而言之,上面的节点也都恢复平衡,那么全树都恢复了平衡。而我们只做了一次“右-右”旋转,只涉及常数个节点,时间消耗O(1),这再好不过了。

    再说“左-左旋转,也叫zig,比如对于这个局部

    先用一个临时指针指向v

    然后让T2成为p的左孩子 

    然后让p成为v的右孩子 

    最后把局部子树的根由p变更为v,临时指针下岗

    至此“左-左”旋转宣告完成,兑换为代码:

    //仅当p存在左孩子时调用这个函数,更新高度并返回新的根
    
    static Position
    SingleRotateWithLeft( Position p )
    {
        Position temp;
        
        temp = p->lc;
        p->lc = temp->rc;
        temp->rc = p;
        
        p->Height = max( Height( p->lc ), Height( p->rc ) ) + 1;
        temp->Height = max( Height( temp->lc ), p->Height ) + 1;
        
        return temp;  /* New root */
    }
    View Code

    上面的算法有一个问题,就是解决的情况都是父子节点在朝向上是一致的,如果朝向不一致呢?单旋转就有心无力了,经过单旋转并不会降低它的深度,就需要引入第二种情况了:

    内侧情形-双旋转

    第二种情况是发生在内侧(左-右or右-左),这需要一次双旋转来完成,其实就是两个单旋转的组合,往往是方向相反的一组单旋转协同工作

     

    先说右-左的情况如下

    我们要抽丝剥茧地做重平衡操作,先看p-v这个局部,都朝向左边,所以首先的思路是对p执行一次顺时针的左-左的单旋转,就变成了这样: 

    到这里,g,v,p三个节点就朝向一致了,那么显然,接下来我们要针对g做一次逆时针的zag旋转,和之前说的过程完全一样:

    T1成为g的右孩子

    g成为v的左孩子(感觉这只蜘蛛要扑过来了2333

    这样就完成了局部的重平衡,当然,这里再把细节展示出来是为了方便深入理解,实际写代码的时候直接调用对应单旋转操作,把g传进去就行了。为了清晰看出效果,做一下整理:

    的确已经恢复平衡,以上可能失衡的祖先也会一并回复平衡。

    // This function can be called only if g has a right 
    // child and g's right child has a left child 
    // Do the right-left double rotation 
    // Update heights, then return new root 
    
    static Position
    DoubleRotateWithRight( Position g )
    {
        // Rotate between p and v, p means g->rc
        g->rc = SingleRotateWithLeft( g->rc );
        
        // Rotate between g and p
        return SingleRotateWithRight( g );
    }
    View Code

    下面再说左-右旋转的情况

    为了重新平衡,就不能让k3继续是根了,不然高度永远降不下来。那么唯一的选择就是让k2作为新的根,如此一来根据BST的性质,我们必须把k1放在左孩子的位置上k3放在右孩子的位置上。具体的做法是对k1,k2这个局部,由于父子朝向都向右,直觉也告诉我们要做一次右-右旋转:B成为k1的右孩子k1这颗子树成为k2的左孩子。最后把k3的左孩子这颗子树的根变更为k2

    具体细微过程前面单旋转的时候说过了,这里就给出拆掉脚手架后的中间成品

    稍微整理一下就更明了了,把k2的高度提上去

     

    到这一步之后,我们再把k3-k2这个局部,由于此时朝向都为左,那么顺理成章做一次左-左旋转:C成为k3的左孩

    k3这颗子树成为k2的右孩子。至此,左-右旋转完成,全树的高度得到了控制。

    // This function can be called only if K3 has a left
    // child and K3's left child has a right child
    // Do the left-right double rotation
    // Update heights, then return new root
    
    static Position
    DoubleRotateWithLeft( Position K3 )
    {
        // Rotate between K1 and K2 
        K3->lc = SingleRotateWithRight( K3->lc );
        
        // Rotate between K3 and K2 
        return SingleRotateWithLeft( K3 );
    }
    View Code

    这四种旋转策略已经覆盖了插入操作失衡的所有情况,下面给出总的插入操作,汇总了这四种情况。

    AvlTree
    Insert( int X, AvlTree T )
    {
        if( !T ){//这里是实质的插入部分,无中生有
            //创建并返回一个单节点树
            T = (Position)malloc( sizeof( struct AvlNode ) );
            if( !T ) printf("Fatal Error: Out Of Space!
    ");//错误检测
            else{
                T->value = X;
                T->Height = 0;
                T->lc = T->rc = nullptr;
            }
        }
        
        //还未走到应插入的地点时
        else
            if( X < T->value ) //遵循BST的规则,new value < root value,往左走
            {
                T->lc = Insert( X, T->lc );//此时插入完成后,t指向被插入节点的父亲
                if( Height( T->lc ) - Height( T->rc ) == 2 )
                     //如果新插入节点后lc比rc深2层,那么就是情形1,2
                    if( X < T->lc->value )//如果是这样,根据BST规则,是左-左
                        T = SingleRotateWithLeft( T );
                    else //否则是左-右
                        T = DoubleRotateWithLeft( T );
             /*
              我们需要根据情况去采取不同的旋转策略,使其恢复平衡
              单旋转调整了情形1:发生在外侧,对a的lc->lc插入
              双旋转调整了情形2:发生在内侧,对a的lc->rc插入
              */
    
            }
            else
                if( X > T->value )
                {
                    T->rc = Insert( X, T->rc );
                    //遵循BST的规则,new value > root value,往右走
                    
                    if( Height( T->rc ) - Height( T->lc ) == 2 )
                        //如果新插入节点后右子树更高,那么就是情形3,4
                        if( X > T->rc->value )  //如果是这样,根据BST规则,是右-右
                            T = SingleRotateWithRight( T );
                        else //否则是右-左
                            T = DoubleRotateWithRight( T );
                    /*
                     这个分支里
                     单旋转调整了情形3:发生在外侧,对a的rc->rc插入
                     双旋转调整了情形4:发生在内侧,对a的rc->lc插入
                    */
                }
        
        /* Else X is in the tree already; we'll do nothing */
        
        T->Height = max( Height( T->lc ), Height( T->rc ) ) + 1;
        return T;
    }

    最后可以做一个很直观的比较:分别构建大数据量的BST和AVLT,比较他们的高度,就可以明显看出平衡操作对于高度的有效控制了,给一个完整版本的实现吧,可以对比下和普通BST的层数差距。 

      1 #include "avltree.h"  //这里只给出.c的部分,头文件就是前文的类型声明+各种函数签名
      2 #include <stdlib.h>
      3 #include <stdio.h>
      4 #include <time.h>
      5 
      6 
      7 int max(int a,int b){return a>b?a:b;}
      8 
      9 
     10 
     11 int updateH(AvlTree x){
     12     return x->Height = 1 + max ( Height ( x->lc ), Height ( x->rc ) );
     13 }
     14 
     15 
     16 AvlTree
     17 MakeEmpty( AvlTree T )
     18 {
     19     if( T != NULL )
     20     {
     21         MakeEmpty( T->lc );
     22         MakeEmpty( T->rc );
     23         free( T );
     24     }
     25     return NULL;
     26 }
     27 
     28 
     29 
     30 
     31 void Preorder(Position root);
     32 
     33 int main() {
     34     srand(time(NULL));
     35     AvlTree a=nullptr;
     36     int nodeCnt,del;
     37     printf("Please input how many nodes in the avl tree: ");
     38     scanf("%d",&nodeCnt);
     39     for(int i=0;i<nodeCnt;i++) a=Insert(rand()%(nodeCnt<<1), a);
     40     Preorder(a);
     41     printf("
    
    The height of avlt with %d nodes is : %d
    ",nodeCnt,Height(a));
     42 //    scanf("%d",&del);
     43 //    DeleteInAVL(del, a);
     44 //    Preorder(a);
     45 }
     46 
     47 Position
     48 Find( int X, AvlTree T )
     49 {
     50     if( !T )
     51         return NULL;
     52     if( X < T->value ){
     53         return Find( X, T->lc );
     54     }
     55     else
     56         if( X > T->value )
     57             return Find( X, T->rc );
     58         else
     59             return T;
     60 }
     61 
     62 Position
     63 FindMin( AvlTree T )
     64 {
     65     if( !T )
     66         return NULL;
     67     else
     68         if( T->lc == NULL )
     69             return T;
     70         else
     71             return FindMin( T->lc );
     72 }
     73 
     74 Position
     75 FindMax( AvlTree T )
     76 {
     77     if( T != NULL )
     78         while( T->rc != NULL )
     79             T = T->rc;
     80     
     81     return T;
     82 }
     83 
     84 
     85 // This function can be called only if g has a left child
     86 // Perform a rotate between a node (g) and its left child
     87 // Update heights, then return new root
     88 
     89 static Position
     90 SingleRotateWithLeft( Position p )   //左-左的情况
     91 {
     92     Position temp;
     93     
     94     temp = p->lc;
     95     p->lc = temp->rc;
     96 
     97     temp->rc = p; 
     98     
     99     p->Height = max( Height( p->lc ), Height( p->rc ) ) + 1;
    100     temp->Height = max( Height( temp->lc ), p->Height ) + 1;
    101     
    102     return temp;  /* New root */
    103 }
    104 
    105 
    106 // This function can be called only if g has a right child
    107 // Perform a rotate between a node (g) and its right child
    108 // Update heights, then return new root
    109 
    110 static Position
    111 SingleRotateWithRight( Position g )   //右-右的情况
    112 {
    113     Position temp;
    114     
    115     temp = g->rc;
    116     g->rc = temp->lc;
    117 
    118     temp->lc = g;     
    119     
    120     g->Height = max( Height( g->lc ), Height( g->rc ) ) + 1;
    121     temp->Height = max( Height( temp->rc ), g->Height ) + 1;
    122     
    123     return temp;  /* New root */
    124 }
    125 
    126 
    127 // This function can be called only if K3 has a left
    128 // child and K3's left child has a right child
    129 // Do the left-right double rotation
    130 // Update heights, then return new root
    131 
    132 static Position
    133 DoubleRotateWithLeft( Position K3 )   //左-右的情况
    134 {
    135     /* Rotate between K1 and K2 */
    136     K3->lc = SingleRotateWithRight( K3->lc );
    137 
    138     
    139     /* Rotate between K3 and K2 */
    140     return SingleRotateWithLeft( K3 );
    141 }
    142 
    143 // This function can be called only if g has a right
    144 // child and g's right child has a left child
    145 // Do the right-left double rotation
    146 // Update heights, then return new root
    147 
    148 static Position
    149 DoubleRotateWithRight( Position g )   //右-左的情况
    150 {
    151     // Rotate between p and v, p means g->rc
    152     g->rc= SingleRotateWithLeft( g->rc );
    153 154     
    155     // Rotate between g and p
    156     return SingleRotateWithRight( g );
    157 }
    158 
    159 
    160 AvlTree
    161 Insert( int X, AvlTree T )
    162 {
    163     Position p;// it means p on the "new node"
    164     if( !T ){//这里是实质的插入部分,无中生有
    165         //创建并返回一个单节点树
    166         T = (Position)malloc( sizeof( struct AvlNode ) );
    167         if( !T ) printf("Fatal Error: Out Of Space!
    ");//错误检测
    168         else{
    169             T->value = X;
    170             T->Height = 0;
    171             T->lc = T->rc = nullptr;
    172         }
    173     }
    174     
    175     //还未走到应插入的地点时
    176     else
    177         if( X < T->value ) //遵循BST的规则,new value < root value,往左走
    178         {
    179             T->lc = Insert( X, T->lc );
    180             //此时插入完成后,T指向被插入节点的父亲,新生节点作为T的左孩子而存在。
    181             
    182             if( Height(T->lc)-Height(T->rc) == 2 )
    183                 //如果新插入节点后lc比rc深2层,那么就是情形1,2
    184                 if( X < T->lc->value )//如果是这样,根据BST规则,是左-左
    185                     T = SingleRotateWithLeft( T );
    186                 else //否则是左-右
    187                     T = DoubleRotateWithLeft( T );
    188             /*
    189              我们需要根据情况去采取不同的旋转策略,使其恢复平衡
    190              单旋转调整了情形1:发生在外侧,对a的lc->lc插入
    191              双旋转调整了情形2:发生在内侧,对a的lc->rc插入
    192              */
    193             
    194             
    195         }
    196         else
    197             if( X > T->value ) //遵循BST的规则,new value > root value,往右走
    198             {
    199                 T->rc = Insert( X, T->rc );
    200                 //此时插入完成后,T指向被插入节点的父亲,新生节点作为T的右孩子而存在。
    201                 if( Height(T->rc)-Height(T->lc) == 2 )
    202                     //如果新插入节点后右子树更高,那么就是情形3,4
    203                     if( X > T->rc->value )  //如果是这样,根据BST规则,是右-右
    204                         T = SingleRotateWithRight( T );
    205                     else //否则是右-左
    206                         T = DoubleRotateWithRight( T );
    207                 /*
    208                  这个分支里
    209                  单旋转调整了情形3:发生在外侧,对a的rc->rc插入
    210                  双旋转调整了情形4:发生在内侧,对a的rc->lc插入
    211                  */
    212                 
    213                 //
    214             }
    215     
    216     /* Else X is in the tree already; we'll do nothing */
    217     
    218     updateH(T);
    219     return T;
    220 }
    221 
    222 
    223 int
    224 Retrieve( Position P )
    225 {
    226     return P->value;
    227 }
    228 
    229 
    230 void Preorder(Position root){
    231     if (root) {
    232         printf("%d ",root->value);
    233         Preorder(root->lc);
    234         Preorder(root->rc);
    235     }
    236 }

    祝食用愉快2333

    ps.转载请注明文章来源,否则会追加法律责任。

  • 相关阅读:
    我的后端开发书架2015 2.0版
    Java根据字节数据判断文件类型
    基于lucene的案例开发:查询语句创建PackQuery
    Web 前沿——HTML5 Form Data 对象的使用
    基于HTML5的可预览多图片Ajax上传
    【面试】Spring问答Top 25
    【劳动节江南白衣Calvin 】我的后端开发书架2015
    【转载】Java 动态代理
    使用iframe给页面的localStorage扩容
    js中的||和&&使用技巧
  • 原文地址:https://www.cnblogs.com/hongshijie/p/9341324.html
Copyright © 2011-2022 走看看