[演算法] 資料結構之AVL樹

weixin_34037977發表於2016-04-04

 

1 .基本概念

AVL樹的複雜程度真是比二叉搜尋樹高了整整一個數量級——它的原理並不難弄懂,但要把它用程式碼實現出來還真的有點費腦筋。下面我們來看看:

1.1  AVL樹是什麼?

AVL樹本質上還是一棵二叉搜尋樹(因此讀者可以看到我後面的程式碼是繼承自二叉搜尋樹的),它的特點是:

1. 本身首先是一棵二叉搜尋樹。 

2. 帶有平衡條件:每個結點的左右子樹的高度之差的絕對值(平衡因子)最多為1。 

例如:

     5              5

    \            \

   2   6          2   6

  \   \        \

 1   4   7      1   4

    /              /

   3              3

上圖中,左邊的是AVL樹,而右邊的不是。因為左邊的樹的每個結點的左右子樹的高度之差的絕對值都最多為1,而右邊的樹由於結點6沒有子樹,導致根結點5的平衡因子為2。

1.2  為什麼要用AVL樹?

有人也許要問:為什麼要有AVL樹呢?它有什麼作用呢?

我們先來看看二叉搜尋樹吧(因為AVL樹本質上是一棵二叉搜尋樹),假設有這麼一種極端的情況:二叉搜尋樹的結點為1、2、3、4、5,也就是:

 1

  \

   2

    \

     3

      \

       4

        \

         5

聰明的你是不是發現什麼了呢?呵呵,顯而易見——這棵二叉搜尋樹其實等同於一個連結串列了,也就是說,它在查詢上的優勢已經全無了——在這種情況下,查詢一個結點的時間複雜度是O(N)!

好,那麼假如是AVL樹(別忘了AVL樹還是二叉搜尋樹),則會是:

   2

  \

 1   4

    \

   3   5

可以看出,AVL樹的查詢平均時間複雜度要比二叉搜尋樹低——它是O(logN)。也就是說,在大量的隨機資料中AVL樹的表現要好得多。

1.3  旋轉

假設有一個結點的平衡因子為2(在AVL樹中,最大就是2,因為結點是一個一個地插入到樹中的,一旦出現不平衡的狀態就會立即進行調整,因此平衡因子最大不可能超過2),那麼就需要進行調整。由於任意一個結點最多隻有兩個兒子,所以當高度不平衡時,只可能是以下四種情況造成的:

1. 對該結點的左兒子的左子樹進行了一次插入。 

2. 對該結點的左兒子的右子樹進行了一次插入。 

3. 對該結點的右兒子的左子樹進行了一次插入。 

4. 對該結點的右兒子的右子樹進行了一次插入。 

情況1和4是關於該點的映象對稱,同樣,情況2和3也是一對映象對稱。因此,理論上只有兩種情況,當然了,從程式設計的角度來看還是四種情況。

第一種情況是插入發生在“外邊”的情況(即左-左的情況或右-右的情況),該情況可以通過對樹的一次單旋轉來完成調整。第二種情況是插入發生在“內部”的情況(即左-右的情況或右-左的情況),該情況要通過稍微複雜些的雙旋轉來處理。

1.31  旋轉

情況1對該結點的左兒子的左子樹進行了一次插入。

左邊為調整前得節點,我們可以看出k2的左右子樹已不再滿足AVL平衡條件,調整後的為右圖。

我們可以看出,解決辦法是將x上移一層,並將z下移一層,由於在原樹中k2 > k1,所以k2成為k1的右子樹,而y是小於k2的,所以成為k2的左子樹。

為了設計演算法,我們這裡來看一個更易理解的:插入的是節點“6

演算法設計:由於是情形1對該結點的左兒子的左子樹進行了一次插入,該節點是“8”,首先我們不考慮其父節點的情況,因為我們建立節點是遞迴建立的,可以不用考慮其父節點與其的連線,這在後面遞迴建立的時候會說到,由於“8”的右孩子將不會發生變化,但是其左孩子設為“7”的右孩子,將7的右孩子設為“8”及其子樹,然後返回“7”節點的指標。

實現程式碼:

//情形

AVLTree SingleRotateWithLeft(PAVLNode k2)

{

 PAVLNode k1;

   k1 = k2->l;

     k2->l = k1->r;

     k1->r = k2;

 

     k2->h = MAX( Height( k2->l ), Height( k2->r ) ) + 1;

     k1->h = MAX( Height( k1->l ), k2->h ) + 1;

 

     return k1;  /* New root */

 

}

情況4:對該結點的右兒子的右子樹進行了一次插入。 

左邊為調整前得節點,我們可以看出k1的左右子樹已不再滿足AVL平衡條件,調整後的為右圖。

我們可以看出,解決辦法是將z上移一層,並將x下移一層,由於在原樹中k2 > k1,所以k1成為k2的左子樹,而y是大於k1的,所以成為k1的右子樹。

為了設計演算法,我們這裡來看一個更易理解的:插入的是節點“6

 

演算法設計:由於是情形1對該結點的右兒子的右子樹進行了一次插入,該節點為“4”,我們同第一種情形類似。

實現程式碼:

//情形4

AVLTree SingleRotateWithRight(PAVLNode k1)

{

PAVLNode k2;

  k2 = k1->r;

    k1->r = k2->l;

    k2->l = k1;

 

    k1->h = MAX( Height( k1->l ), Height( k1->r ) ) + 1;

    k2->h = MAX( Height( k2->r ), k1->h ) + 1;

 

    return k2;  /* New root */

 

}

 

1.32  旋轉

情況2:對該結點的左兒子的右子樹進行了一次插入。 

這種情況是單旋轉調整不回來的,如下圖:

圖(1

 

--右雙旋轉如下:

圖(2

 

這裡我們將圖(1)的Y子樹看成如圖(2),以k2為子樹根節點的樹,我們將其子樹分成比D,這裡我我先對k3的左子樹進行一次情形四的右旋轉,然後在進行一次情形1的左旋轉,詳細步驟如下:(紅色框裡面的即是要進行單旋轉的)

 

實現程式碼:

//情形2

AVLTree DoubleRotateWithLeft( PAVLNode k3 )

{

            /* Rotate between K1 and K2 */

            k3->l = SingleRotateWithRight( k3->l );

 

            /* Rotate between K3 and K2 */

            return SingleRotateWithLeft(k3);

}

情況3對該結點的右兒子的左子樹進行了一次插入。

 

右—左雙旋轉如下:

我們先對k1的右子樹進行一次左旋轉(情形1,然後再對k1進行一次右旋轉(情形4)。

實現程式碼:

//情形3

AVLTree DoubleRotateWithRight( PAVLNode k1 )

{

            /* Rotate between K3 and K2 */

            k1->r = SingleRotateWithLeft( k1->r );

 

            /* Rotate between K1 and K2 */

            return SingleRotateWithRight( k1 );

}

 

1.3  插入操作

插入的核心思路是通過遞迴找到合適的位置,插入新結點,然後看新結點是否平衡(平衡因子是否為2),如果不平衡的話,就分成三種大情況以及兩種小情況:

1. 在結點的左兒子(X < T->item) 

o 在左兒子的左子樹 (X < T->l-> item),“外邊”,要做單旋轉。 

o 在左兒子的右子樹 (X > T->l-> item),“內部”,要做雙旋轉。 

2. 在結點的右兒子(X > T->item) 

o 在右兒子的左子樹(X < T->r-> item),“內部”,要做雙旋轉。 

o 在右兒子的右子樹(X > T->r-> item),“外邊”,要做單旋轉。 

3. (X == T->item) ,對該節點的計數進行更新。

當進行了旋轉之後,必定會有結點的“父結點”是需要更新的,例如:

   2

  \

 1   4

    \

   3   5

        \

         6

上圖是調整前的,下圖是調整後的:

     4

    \

   2   5

  \   \

 1   3   6

可以看出,根結點2不平衡,是由於它的右兒子的右子樹插入了新的結點6造成的。因此,這屬於“外邊”的情況,要進行一次單旋轉。於是我們就把結點4調整上來作為根結點,再把結點2作為4的左兒子,最後把結點2的右兒子修改為原來的結點4的左兒子。

實現程式碼:

AVLTree Insert(Item X, AVLTree T )

 {

            if( T == NULL )

            {

                /* Create and return a one-node tree */

                T = (PAVLNode)malloc( sizeof(AVLNode ) );

                if( T == NULL )

                    perror("malloc failed");

                else

                {

                    T->item = X; 

T->h = 0;

                    T->l = T->r = NULL;

                    T->count = 1;

                }

            }

            else if(compare(&X,&T->item) == -1)//插入情況1

            {

                T->l = Insert( X, T->l );

                if( Height( T->l ) - Height( T->r ) == 2 )

                    if(compare(&X, &T->l->item ) == -1)//左邊左子樹 單旋轉 

                        T = SingleRotateWithLeft( T );

                    else

                        T = DoubleRotateWithLeft( T );//左邊右子樹 

            }

            else if( compare(&X,&T->item) == 1 ) //插入情況2

            {

                T->r = Insert( X, T->r );

                if( Height( T->r ) - Height( T->l ) == 2 )

                    if(compare(&X , &T->r->item) == 1)//右邊右子樹 單旋轉

                        T = SingleRotateWithRight( T );

                    else

                        T = DoubleRotateWithRight( T );//右邊左子樹 

            }

            else//插入情況3

             T->count++;

            /* Else X is in the tree already; we'll do nothing */

 

            T->h = MAX( Height( T->l ), Height( T->r ) ) + 1;

            return T;

}

 

 

 轉自:http://blog.chinaunix.net/uid-25324849-id-2182877.html

相關文章