自動平衡二叉樹的構建-AVL樹

玻璃窗起霧了發表於2021-01-30

AVL 樹是基於二叉搜尋樹的。但是它是自動平衡的,意思是,它左子樹的深度和右子樹的深度差要麼是 0,±1 。沒有其他可能。這就是 AVL 樹,這棵樹長的比較對稱,不會出現極端的一邊倒的情況。這也就意味著 AVL 在建立過程中,根節點也會不斷的變換。 AVL 樹的目的就是為了解決搜尋二叉搜尋樹的時候可能出現最壞複雜度的情況。 AVL 樹極端情況下複雜度也就 log(n)

但是 AVL 樹的建立有點複雜。網上查詢了部分資料,維基百科的 AVL 定義是我主要的參考來源。具體定義可以自行檢視。

在實現過程中,最讓我頭疼的是平衡因子的計算,資料中也沒有說明具體的演算法,或者說了也沒看懂。網上搜尋了一些資料,也是極盡坑爹,要麼含糊不說,要麼一筆帶過。始終不知道如何計算。因此本人在程式中使用的是計算出左右子樹的最大深度,做減法。由於計算左右子樹是一個遞迴演算法,也是相對有些複雜。因此演算法的整體複雜度也隨之增加。

為了降低程式的複雜度,對於 AVL 樹節點的定義資訊,也有所增加,比如增加了指向父節點的指標。當指明孩子的時候,自動的也確定父親。程式碼如下:

1.   public node leftchild   

2.          {   

3.               set    

4.              {   

5.                  _leftchild =  value ;   

6.                   if  (_leftchild !=  null )   

7.                  {   

8.                      _leftchild.father = this;// 確定父結點  

9.    

10.                }   

11.            }   

12.             get    

13.            {   

14.                 return  _leftchild;   

15.            }   

16.        }   

17.        public node rightchild   

18.        {   

19.             set    

20.            {   

21.                _rightchild =  value ;   

22.                 if  (_rightchild !=  null )   

23.                {   

24.                    _rightchild.father = this;// 確定父結點  

25.  

26.                }   

27.            }   

28.             get    

29.            {   

30.                 return  _rightchild;   

31.            }   

32.        }   

33.  

其中 balance 是平衡因子。實現方式如下:

1.   public  int balance   

2.         {   

3.              get    

4.             {   

5.                  return  maxDepth( this .leftchild) - maxDepth( this .rightchild);   

6.             }   

7.         } 

 
AVL
樹構建中的關鍵步驟叫做轉換。分成 4 種情況的轉換。具體的內容可以參考維基百科。本人參照維基百科的圖例寫出 4 個轉換方式。這四種分別是 LR LL RL RR

此處簡述一下 LR 轉換。首先要匹配哪個結點是哪個結點。要找準了,不然就容易出錯。 LR 中,結點 4 轉到結點 5 3 中間,並且結點 B 成為結點 3 右孩子了。其中 LR LL 的時候結點 5 依然是根結點,沒變。那麼程式就如下寫法,其中 root 是根結點引數,但此處的轉換不需要有根結點參與。程式的寫法儘量照圖,把要轉動的點作為 leaf 結點傳進來。即 node3 就是 leaf ,然後依次匹配好哪個是 node4 ,哪個是 node5, 哪個是 B ,全都匹配好之後,把圖上的動作翻譯成程式碼。此處需要對連結串列操作有一定的基礎。懂的自然懂。當 LR 經過圖上的轉換後,變成 LL 型了,那麼直接呼叫 LL 的轉換方式就可以了。

1.   static void LL_To_Balanced( ref   node   root node   leaf )  

2.          {  

3.    

4.               node   node4  = leaf;  

5.               node   node3  = node4.leftchild;  

6.               node   node5  = node4.father;  

7.               node   C  = node4.rightchild;  

8.              // 以上程式碼根據圖把結點給找準了,下面就開始動作了。  

9.    

10.            if (node5.father != null)// 此處涉及到node4變成根結點了。那麼如果node5的father存在的話,要指向node4的。  

11.            {  

12.                if (node5.isleftchild)  

13.                    node5.father.leftchild = node4;  

14.                else if (node5.isrightchild)  

15.                    node5.father.rightchild = node4;  

16.            }  

17.            else 

18.            {  

19.                root = node4;// 如果node5是整棵樹的根結點,那麼現在就變成node4了  

20.                node4.father = null;  

21.            }  

22.            node4.rightchild = node5;  

23.            node5.leftchild = C;  

24.        }  

再看 LL 型的轉換,首先,同樣的找準結點,匹配好哪個是結點 4 ,哪個是結點 5 ,哪個是結點 3 ,那個結點 C ,因為 LL 的轉換也就只涉及到結點 3,4,5,C 這幾個結點。程式碼中,把結點 4 當做 leaf ,因此傳值的時候要傳對了。由於 LL 轉換涉及到樹的根結點的變換,根據圖中的示例,假設 node5 有個 father ,那麼這個 father 現在是把 node4 作為孩子了。至於是左孩子還是右孩子,就看 node5 是左孩子還是右孩子。假設 node5 沒有 father ,也就是說 node5 是根節點,那麼 node4 就成為根結點了。

1.   static void LL_To_Balanced( ref   node   root node   leaf )  

2.   {  

3.    

4.        node   node4  = leaf;  

5.        node   node3  = node4.leftchild;  

6.        node   node5  = node4.father;  

7.        node   C  = node4.rightchild;  

8.       // 以上程式碼根據圖把結點給找準了,下面就開始動作了。  

9.    

10.     if (node5.father != null)// 此處涉及到node4變成根結點了。那麼如果node5的father存在的話,要指向node4的。  

11.     {  

12.         if (node5.isleftchild)  

13.             node5.father.leftchild = node4;  

14.         else if (node5.isrightchild)  

15.             node5.father.rightchild = node4;  

16.     }  

17.     else 

18.     {  

19.         root = node4;// 如果node5是整棵樹的根結點,那麼現在就變成node4了  

20.         node4.father = null;  

21.     }  

22.     node4.rightchild = node5;  

23.     node5.leftchild = C;  

24. }  

RL RR 的轉換也是如上面所述的演算法。關鍵要匹配好那些結點。

本人之前寫過二叉搜尋樹的插入,可以參考前面的文章。 AVL 的建立和二叉搜尋樹是類似的,根據結點的大小插入位子。但是多了一步,就是 要做轉型,也就是上面說的 4 種轉換方式。

那麼程式碼可以先借用構建二叉搜尋樹的程式碼了。插完結點後,呼叫轉型方法不就 OK 了嘛。如下的方法,就是生成了普通的二叉搜尋樹。然後在對其轉換。

1.   // 此處演算法就是插入二叉搜尋樹  

2.          static void InsertIntoAVL( node  root,  node  leaf)  

3.          {  

4.               if  (root == null)  

5.              {  

6.                  root = leaf;  

7.                   return ;  

8.              }  

9.               if  (root.nodevalue == leaf.nodevalue)  

10.            {  

11.                 return ;  

12.            }  

13.             else   if  (root.nodevalue > leaf.nodevalue)  

14.            {  

15.                 if  (root.hasleftchild == false)  

16.                {  

17.                    root.leftchild = leaf;  

18.                }  

19.                 else  

20.                {  

21.                    InsertIntoAVL(root.leftchild, leaf);  

22.                }  

23.            }  

24.             else   if  (root.nodevalue < leaf.nodevalue)  

25.            {  

26.                 if  (root.hasrightchild == false)  

27.                {  

28.                    root.rightchild = leaf;  

29.                }  

30.                 else  

31.                {  

32.                    InsertIntoAVL(root.rightchild, leaf);  

33.                }  

34.            }  

35.        } 

以上透過程式碼插入結點,演算法同二叉搜尋樹的結點插入,接下來確定轉換問題。轉換的時候,要確定,對誰轉換,哪種型別的轉換。

先第一個問題,對誰轉換。那當然是插入了哪個結點,這個結點有可能打破了平衡,因此轉換肯定和該節點有關。在我的程式碼中,以新插入的結點作為參照,依次找它的父節點。(此處就知道為什麼要做 node 的定義中增加 father 屬性了),一旦找到某個父節點的平衡因子是 ±2 ,可以開始處理第二個問題了。此處再強調的是,平衡因子的演算法是左 - 右,因此如果是 +2 ,那麼就是說明左邊更深,反之亦反。

第二個問題,哪種型別的轉換,維基百科上也有現成的說法,拿來照搬就行。簡單的說就是如果一個結點的平衡因子是 ±2 了,那麼就看它的孩子的平衡因子,是 ±1 ,不同的值代表了不同的型別。具體的邏輯,可以看我的程式碼反推。此處要注意的是,傳進去的 leaf 結點,我都是把平衡因子是 ±2 的子結點作為 leaf 結點的。當然你也可以直接就用平衡因子是 ±2 結點作為引數傳遞。

程式碼實現如下,此方法的引數 root 就是表示這顆 AVL 樹的根結點,因為很有可能在構建這棵樹的時候根結點會變化,所以要時刻記錄下,列印樹的時候可以從根結點開始。 leaf 引數就是之前你新插入的結點,根據這個 leaf 為基準找 father ,你可以根據程式碼反推一些邏輯:

1.   // 做旋轉  

2.          static void Revolve(ref node root, node leaf)  

3.          {  

4.              Console.ForegroundColor = ConsoleColor.Yellow;  

5.               if  (root ==  null )// 如果一棵樹是空樹,即沒有根結點的情況下,插入一個葉子,那麼根結點就是這樣葉子,也不需要做旋轉了。  

6.              {  

7.                  root = leaf;  

8.                  Console.ForegroundColor = ConsoleColor.Gray;  

9.                  return;  

10.            }  

11.            node itsfather = leaf.father;  

12.            while (itsfather !=  null )  

13.            {  

14.                 if  (itsfather.balance ==  2 )//LR 或者LL  

15.                {  

16.                     if  (itsfather.leftchild.balance ==  1 )  

17.                    {  

18.                        Console.WriteLine( "LL" );  

19.                        LL_To_Balanced(ref root, itsfather.leftchild);  

20.                    }  

21.                     else   if  (itsfather.leftchild.balance == - 1 )  

22.                    {  

23.                        Console.WriteLine( "LR" );  

24.                        LR_To_Balanced(ref root, itsfather.leftchild);  

25.  

26.                    }  

27.                    Console.ForegroundColor = ConsoleColor.Gray;  

28.                    return;  

29.                }  

30.                 else   if  (itsfather.balance == - 2 )  

31.                {  

32.                     if  (itsfather.rightchild.balance == - 1 )  

33.                    {  

34.                        Console.WriteLine( "RR" );  

35.                        RR_To_Balanced(ref root, itsfather.rightchild);  

36.                    }  

37.                     else   if  (itsfather.rightchild.balance ==  1 )  

38.                    {  

39.                        RL_To_Balanced(ref root, itsfather.rightchild);  

40.                        Console.WriteLine( "RL" );  

41.                    }  

42.                    Console.ForegroundColor = ConsoleColor.Gray;  

43.                    return;  

44.                }  

45.                itsfather = itsfather.father;  

46.            }  

47.  

48.            Console.WriteLine( "no need to revolve" );  

49.            Console.ForegroundColor = ConsoleColor.Gray;  

50.        } 

最後,就是呼叫插入結點的方法。為了讓結果直觀,本人寫了個插入方法,主要列印樹的結構。可自行反推邏輯。  

1.   // 插入結點,列印一些資訊   

2.          private static  node   InsertNode ( ref   node   root node   thenode )   

3.          {   

4.              InsertIntoAVL(root, thenode);   

5.              Console.WriteLine( " 插入了"  + thenode.nodevalue);   

6.              printtree(root);   

7.              Console.WriteLine( " 開始對其旋轉" );   

8.              Revolve( ref  root, thenode);   

9.              Console.ForegroundColor = ConsoleColor.Green;   

10.            Console.WriteLine( " 旋轉後的結構如下" );   

11.            Console.ForegroundColor = ConsoleColor.Gray;   

12.            printtree(root);   

13.            Console.WriteLine( "******END*******" );   

14.            return root;   

15.        } 

Main 方法中,呼叫就如下:

1.   node   node_1  = new  node (1 );   

2.                node   node_2  = new  node (2 );   

3.                node   node_3  = new  node (3 );   

4.                node   node_4  = new  node (4 );   

5.                node   node_5  = new  node (5 );   

6.                node   node_6  = new  node (6 );   

7.                node   node_7  = new  node (7 );   

8.                node   node_8  = new  node (8 );   

9.                node   node_9  = new  node (9 );   

10.              node   node_10  = new  node (10 );   

11.              node   node_11  = new  node (11 );   

12.              node   node_12  = new  node (12 );   

13.              node   node_13  = new  node (13 );   

14.              node   node_14  = new  node (14 );   

15.              node   node_15  = new  node (15 );   

16.              node   node_16  = new  node (16 );   

17.              node   node_17  = new  node (17 );   

18.              node   node_18  = new  node (18 );   

19.              node   node_19  = new  node (19 );   

20.              node   node_20  = new  node (20 );   

21.              node   node_21  = new  node (21 );   

22.              node   node_22  = new  node (22 );  

23.  

24.  

25.              node   root  = null;   

26.             InsertNode( ref  root, node_10);   

27.             InsertNode( ref  root, node_8);   

28.             InsertNode( ref  root, node_5);   

29.             InsertNode( ref  root, node_12);   

30.             InsertNode( ref  root, node_17);   

31.             InsertNode( ref  root, node_9);   

32.             InsertNode( ref  root, node_1);   

33.             InsertNode( ref  root, node_4);  

34.  


結果如下:

具體程式碼,看附件。

後記:

AVL 樹的構建,看起來簡單,但是真的寫起程式碼來還是很麻煩。思路清晰是關鍵。還有很多要改進的地方,比如,平衡因子的計算,本人演算法實在不算高明。還有最佳化的餘地,比如回溯法。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69992957/viewspace-2754794/,如需轉載,請註明出處,否則將追究法律責任。

相關文章