《編程珠璣,字字珠璣》讀書筆記完結篇——AVL樹


寫在最前面的

手賤翻開了《珠璣》的最后幾章,所以這一篇更多是關於13、14、15章的內容。這篇文章的主要內容是“AVL樹”,即平衡樹,比紅黑樹低一個等次。搗亂真惹不起紅黑樹,情況很復雜;而AVL思路比較清晰。《編程珠璣,字字珠璣》910讀書筆記——代碼優化更新了,做了點關於“哨兵”的筆記。在這篇文章的末尾,筆者還加了對引用調用的“大徹大悟”。

4篇讀書筆記:全在這里

AVL樹

學習數據結構的時候,有過一次實驗課, 題意大概:英文單詞出現次數統計。當時選了哈希表,映射(map),AVL樹(平衡樹)三種方法來做,是沖着“完成實驗老師請吃飯”去做的。哈希表鍵值用“除留余數法”,處理沖突用了最簡單的開哈希表的“鏈地址法”。 映射(map)沒有深入,只是簡單的應用。 比較痛心的是AVL樹。

AVL樹的旋轉

樹的旋轉分四種:左單旋,右單旋,左右旋轉,右左旋轉。規定,右子樹的高度減去左子樹的高度得到此節點的平衡數(也叫平衡因子,balance factor,bf),用bf(node)表示node節點的平衡數。小剖一下這四種情況:

當bf(node)==2的時候,即右子樹高度比左子樹高,需要將樹在node節點左單旋。在作旋轉之后,左子樹bf+1,右子樹bf-1,node節點平衡數歸零。
image
節點的調整過程很清晰。

 

再來當bf(node)==-2時候,即右子樹比左子樹低。需要將樹在node節點右單旋。在作選擇之后,左子樹bf-1,右子樹+1,node節點平衡樹歸零。
image 
細心的發現,左單旋和右單旋是一樣的,只是反過來罷了。

 

下面的情況復雜了點,但是他們是從上面兩種情況延伸過來的,但是這種變化導致它們平衡化的方法也有小小不同。 下面兩種情況從子樹的內側插入,導致子樹(bf(kid))和其父親(bf(parent))的bf正負相反,先來左右旋轉,看圖:

image
解決之道:kid節點作簡單的左單旋,然后parent作簡單的右單旋。在過程中需要非常注意節點bf的調整,要分情況進行討論(把這個檻跨過去,離成功就不遠了)。

  • 如果從左kid的右子樹(grandkid)的左側插入,
    對bf(kid)調整:那么bf(grandkid)<0,在kid作了左單旋之后,grandkid的左側樹被調整為kid的右子樹,結果bf(kid)=0;
    對bf(parent)調整:在對parent作了右單旋之后,grandkid右子樹被調整為parent的左子樹,因此如果bf(grandkid)<0,那么bf(parent)=1; 

  • 如果從左kid的右子樹(grandkid)的右側插入,
    對bf(kid)調整:那么bf(grandkid)>0,在kid作了左單旋之后,grandkid的左側樹被調整為kid的右子樹,結果bf(kid)=-1;
    對bf(parent)調整:在對parent作了右單旋之后,grandkid右子樹被調整為parent的左子樹,因此如果bf(grandkid)<0,那么bf(parent)=0;
  • 對bf(grandkid)調整:最后,grandkid被調整為新樹的根節點,bf(grandkid)=0。

(作一個填空題吧) 結合下面的圖來做,屬於右左旋轉:

 
如果從右kid的左子樹(grandkid)的左側插入,
對bf(kid)調整:那么bf(grandkid)    0,在kid作了左單旋之后,grandkid的左側樹被調整為kid的右子樹,結果bf(kid)=   
對bf(parent)調整: 在對parent作了右單旋之后,grandkid右子樹被調整為parent的左子樹,因此如果bf(grandkid)    0,那么bf(parent)=   
 
如果從右kid的左子樹(grandkid)的 右側插入,
對bf(kid)調整:那么bf(grandkid)    0,在kid作了左單旋之后,grandkid的左側樹被調整為kid的右子樹,結果bf(kid)=   
對bf(parent)調整:在對parent作了右單旋之后,grandkid右子樹被調整為parent的左子樹,因此如果bf(grandkid)    0,那么bf(parent)=   

對bf(grandkid)調整:最后,grandkid被調整為新樹的根節點,bf(grandkid)=   

答案:<,1,<,0;>,0,>,-1。

 

可以看出三個節點在調整過程中需要更改bf。最后一種旋轉就是右左旋轉。不需要太多的分析,跟上面的是一樣的,做一個簡單的反轉。搗亂上圖:

image

構造一個平衡樹,即不斷將一個新的節點在原樹中找到合適的位置,然后調整。那么在“找”的過程中,所經歷的節點bf都改變了(+1或者-1)。插入一個節點的做法是: 用棧存儲所走過的節點,在找到插入位置后,從插入位置的父節點開始調整,如果此父節點是平衡的,那么從棧中取出父節點,繼續調整。

從上面的分析中,只要旋轉后,結果旋轉的節點都會得到bf(node)=0結果,所以只要旋轉后,我們的目的就達到了——樹平衡了!所以bf(node)==0d的節點會越來越多,而且是堆積在樹的頂層。

image

因此,不需要每次都調整到樹的根節點root,只要調整的節點bf=0,就可以結束了,上面的節點或者兄弟節點已經bf=0。這我在剛接觸AVL的時候也很迷惑的地方。

最后我把insert節點的代碼給出:

/***********************************
 ** sample
 **********************************/
void avl::insert(int data) 
{ 
    node * parent = 0,* p = root,* t = new node(data); 
    stack<node *> s; 
    while (p) 
    { 
        int ret = p->comp(*t); 
        if(ret==0)    {delete t;    return;}     
        parent = p;s.push(parent); 
        if(ret<0)     
            p = p->right; 
        else    p = p->left; 
    }// while 

    p = t; 
    assert(p); 

    if (!root) 
    { 
        root = p; 
        return; 
    }// if 

    if(parent->comp(*t)>0)     
        parent->left = p; 
    else    parent->right = p; 

    while (!s.empty()) 
    { 
        parent = s.top(); 
        s.pop(); 

        if(p==parent->left)    parent->bf--; 
        else    parent->bf++; 

        int d; 
        if(parent->bf==0)    break; 
        if(abs(parent->bf)==1)    p=parent; 
        else 
        { 
            d =  parent->bf<0?-1:1; 
            if(d<0 && p->bf<0)    r(parent); 
            else if(d>0 && p->bf>0)    l(parent); 
            else if(d>0 && p->bf<0)    rl(parent); 
            else lr(parent); 
            break; 
        }// if 
    }// while 

    if (s.empty()) 
        root = parent; 
    else 
    { 
        p = s.top(); 
        if(p->comp(*parent)>0) 
            p->left = parent; 
        else 
            p->right = parent; 
    }// if 
}

另外,旋轉的代碼我放在附件里面(如果都貼出來顯得很臃腫),再者,附件里有一個“單詞統計”的實驗報告,有興趣的同學可以下載看看。當時做實驗的時候,AVL統計單詞還是挺給力的:

image

 

漫談引用調用

注意:ANSI C里不支持引用調用,而C++提供了引用調用的實現。
正如《effective c++》條款1提及的,指針和引用有應用上的區別。指針所指的對象可以隨意更改,而且它的指向可以為null,非常靈活;但引用必須代表一個對象,不能為null,而且它被賦予某個對象后,它將始終代表那個對象知道被銷毀為止。例如:
 

/***********************************
 ** sample
 **********************************/
 int b = 1;   
 int &a = b; 

a成為了b的引用,a將不能再引用其他數據。另外,引用變量是否占有內存聽說唯有定義http://topic.csdn.net/u/20100622/15/728477fe-92ab-4e83-8572-0923d37186f1.html),筆者認為可行的方法是程序只在在變量的符號表中添加a,而並沒有為a分配任何的內存。

在函數傳參的過程中,有值傳遞,指針傳遞(都屬於c)和引用傳遞方式(c++)。指針所能做到的,引用也可以做得到。但引用更安全(不至於讓它為null),操作起來更方便,同時擁有和指針優點——“節能減排”。來看看:

/***********************************
 ** sample
 **********************************/
function(TYPE * a)  
    a = new TYPE  
    ****  
main()  
    TYPE * a = NULL;  
    function(a);     
    *** 

在function返回后,a依舊為原來的NULL,並沒有改變。因為你想,function函數棧內,只保存了指針a的原值NULL,即使a = new TYPE能為a賦予新址,但此a非彼a,在function退棧后,此a將被銷毀,而彼a仍舊為NULL。因此如果想更改a指針的內容,必須使用指針的指針或者指針的引用,指針的引用會比較方便。

/***********************************
 ** sample
 **********************************/
function(TYPE *& a)  
    a = new TYPE  
    ****  
main()  
    TYPE * a = NULL;  
    function(a);     
    *** 

這時,指針a的值才有所改變。AVL樹的程序里有較多的引用調用,讀者要注意。搗亂納悶,這筆記,這大徹大悟,應早在大一就應該寫下,羞愧於心,貽笑大方吶。

關於珠璣的總結

珠璣我到底還是把它當作休閑讀物了,對於算法或者數據結構的初學者,這一本是力薦的。

 

附件:

本文完 Thursday, April 26, 2012

搗亂小子 http://daoluanxiaozi.cnblogs.com/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM