詳解平衡二叉樹的失衡型別劃分及調整策略設計

神奇少女祝XiXi發表於2021-04-04

1. 平衡二叉樹

平衡二叉樹

對於樹中的每個節點要求:

  • 左子樹和右子樹的深度差不超過1
  • 左右子樹都是平衡二叉樹

 

平衡因子 = 左子樹深度 - 右子樹深度

==> 在一棵平衡二叉樹中,所有節點的平衡因子只可能有三種取值:-1, 0, 1

 

2. 失衡原因分析及失衡情況分類

平衡二叉樹是一種特殊的二叉排序樹,插入新節點的方法與在二叉排序樹中插入節點相同:先查詢,然後在查詢失敗的位置插入新節點。

但是在一棵平衡二叉樹中新插入一個節點可能會導致樹的失衡,因此每次插入新節點之後要對樹進行調整。

 

書上和網上的資料很多,但大部分都只給出了最終的結論,沒有給出為什麼要這樣做的原因,現在我試圖用自己的方式來理解AVL樹的調整方法:

 

1. 在平衡二叉樹中新插入節點會造成樹中某些節點平衡因子的改變,從而有失衡的風險。

==> 只有插入路徑上的節點的平衡因子可能會改變。

==> 在插入路徑上的節點中,只有那些原本的平衡因子為1, -1的節點可能會失衡(平衡因子變為2)。

==> 原本平衡因子為1的節點失衡後平衡因子會變為2;原本平衡因子為-1的節點失衡後平衡因子會變為-2。並且這兩種情況是對稱的。

2. 在插入路徑上可能會有多個節點失衡,但是高層節點的失衡是由低層節點的失衡造成的,因此存在一個最低失衡節點,只要將這個最低失衡節點調整平衡,並且保證以該節點為根的子樹的高度和原來一樣,那麼高層節點的失衡就會自動恢復。

3. 所謂對失衡節點的調整,其實就是在已知一些子樹和節點相互之間的大小關係以及他們的高度等資訊時用這些子樹和節點重新組裝成一棵滿足平衡二叉樹要求的樹

 

下面僅考慮最低失衡節點原本的平衡因子為1的情況:

==> 該節點失衡後平衡因子變為2,說明新節點的插入導致該節點的左子樹的高度增加了1,這也間接說明了新節點插在了該節點的左子樹上。

==> 插在該節點的左子樹上有兩種可能的情況:①該節點原本就沒有左孩子,②該節點原本是有左孩子的。

==> 情況①不可能存在,因為該節點原本的平衡因子為1,而 平衡因子 = 左子樹深度 - 右子樹深度,所以左子樹的深度至少為1。

 

我們來重新梳理一下目前掌握的結論:

新節點(記為S)一定插在最低失衡節點(記為A)的左子樹上,並且A原本一定有左孩子(記為B)。因為S的插入導致以B為根的子樹整體高度增加1,從而導致了A節點的失衡。

 

還有另外一個重要的結論是:B原本的平衡因子一定為0,證明如下:


 

  首先,因為B原本是平衡的,所以其平衡因子只有三種可能的取值:-1, 0, 1。

  1. 如果B原本的平衡因子為-1或1,並且插入S之後以B為根的子樹整體高度增加1,那麼有以下兩種可能的情況:

  

 

 

   在這兩種情況下,插入新節點之後B都會失衡,這樣的話A就不是最低失衡節點了,與假設矛盾,因此這兩種情況都不成立。

  2. 如果原本B的平衡因子為0,則有以下兩種情況,這兩種情況都成立:

  

  綜上,B原本的平衡因子一定為0,證畢。

 

 

至此,我們將A原本的平衡因子為1時的可能的情況分成了兩種:

 

 

 Case1就是所謂的 "LL型",Case2就是所謂的 "LR型"。

 

根據A和B的平衡因子可以很容易地得出結論:BL、BR、AR的高度相等。證明如下:


B原本的平衡因子為0

==> BL的高度與BR相等,記為n

==> 以B為根的子樹原本的高度為n+1

==> 插入S後以B為根的子樹高度變為n+2

插入S後A的平衡因子為2

==> (n+2) - depth(AR) = 2

==> depth(AR) = n = depth(BL) = depth(BR),證畢。

 

有了這些資訊之後,開始制定調整策略。

 

3. 失衡調整策略

首先,對於調整策略有幾點要明確的:

1. 調整物件:以最低層失衡節點為根的子樹

2. 調整目標:①恢復平衡(根節點平衡因子<=1) ②高度不變(高度恢復為失衡前的高度)

3. 調整依據(調整約束):滿足二叉排序樹的大小關係(即任意一個節點的左子樹上的節點都小於該節點,右子樹上的節點都大於該節點)

 

(3.1)LL型

Step1: 先分析失衡後 子樹BL,BR,AR和節點相互之間的大小關係:

      雖然插入S後以A為根的子樹失衡了,但是仍然滿足平衡二叉樹的約束,即:

      (BL+S) < B < (BR) < A < (AR)

    加括號代表這棵樹上的所有節點。

Step2: 記插入S前以A為根的子樹的高度為H,則插入S後:

      depth(BR) = depth(AR) = H-2

           depth(BL+S) = H-1 

Step3: 根據上述結論設計調整策略如下:

  0. 記錄A的父親節點FA,如果A為根節點則FA=NULL;

  1. 以A為根節點,B->right和A->right分別作為左子樹和右子樹構建新樹TreeA;

  2. 以B為根節點,B->left和TreeA分別作為左子樹和右子樹構建新樹TreeB;

  3. 如果FA==NULL,則令root指向B;

      否則,如果FA->left == A則令FA->left指向B,如果FA->right == A則令FA->right指向B。

 

      在該調整策略中:調整後仍滿足Step1得出的大小關係,並且調整後樹高仍為H。

 

      觀察該策略發現,其實相當於以B為軸,對A做了依次順時針旋轉。這也就是我們常說的 “右旋”。

(3.2)LR型

 

按照和LL型相同的分析方法,當進行到Step3的時候會發現:如果滿足了Step1得出的大小關係,就無法得出使得樹高不變的策略。

所以,我們需要對LR型的情況做進一步細化:

 

對LR型的進一步細分:

因為新節點S插在了B的右子樹上,那我們就先分為兩種情況:①失衡前B的右子樹為空,②失衡前B的右子樹不為空。

這裡和之前不同,情況①時可能成立的。之前在對"A失衡前左子樹是否為空"進行討論時的前提限制是:A失衡前的平衡因子為1,所以A的左子樹不可能為空。但是B失衡前的平衡因子是0,所以它失衡前的右子樹可以為空。如果B失衡前右子樹BR為空,根據之前的結論,可以得出BL、AR也為空。

對於情況②,既然失衡前B的右子樹不為空,不妨設B的右孩子為C節點,因此根據S是插在了C節點的左子樹還是右子樹上又可以將情況②再分為兩種子情況。

最終我們將LR型細分為3種子情況:

 

 

之後在對每一種子情況按照和LL型相同的分析方法分析其調整策略,結論如下:

 

 

觀察後發現,這三種調整策略可以歸納為一種:

  0. 記錄A的父親節點FA,如果A為根節點則FA=NULL;

  1. 以B為根節點,B->left和C->left分別作為左子樹和右子樹構建新樹TreeB;

  2. 以A為根節點,C->right和A->right分別作為左子樹和右子樹構建新樹TreeA;

  3. 以C為根節點,TreeB和TreeA分別作為左子樹和右子樹構建新樹TreeC;

  4. 如果FA==NULL,則令root指向C;

      否則,如果FA->left == A則令FA->left指向C,如果FA->right == A則令FA->right指向C。

 

觀察該策略,是不是很像:先對B做一次逆時針旋轉,再對A做一次順時針旋轉 ?這就是我們常說的 “先左旋後右旋”。

 

(3.3)判斷失衡型別

至此,對於LL型和LR型的失衡調整策略就已經設計出來了,下面還有一個問題就是怎麼判斷究竟是哪一種失衡型別呢?

這個問題其實很簡單,根據失衡後A和B的平衡因子就可以做出判斷:

A的平衡因子等於2, B的平衡因子等於1   ==> LL型

A的平衡因子等於2, B的平衡因子等於-1  ==> LR型  

 

 

對於RR型和RL型的失衡策略和判斷失衡型別可以按照同樣的方法得出,由於本文的目的在於對這些結論是如何得出的進行解釋,至於具體結論網上有很多的資料可以參考,就不再贅述。在此分享一下耿國華老師主編的《資料結構——C語言描述》,這本書裡面有非常非常詳細的講解和完整程式碼,可以供大家參考。

 

4. 在AVL樹中插入節點的完整步驟

最後,再梳理一下在一棵AVL樹中插入一個新節點的完整步驟:

1. 查詢新節點S應插入的位置,並將S節點插入(插入方法與在二叉排序樹中插入一個節點相同);

    同時記錄:

  • 距S的插入位置最近且平衡因子等於1或-1的節點A  =>  如果新節點的插入引起失衡的話,A即為最低層失衡節點;
  • 以及節點A的父親節點FA => 用於之後將調整後的失衡子樹插入樹中。

2. 修改從A到S路徑上各節點的平衡因子。

    解釋:只需要修改從A到S路徑上各節點的平衡因子,而不是整條查詢路徑上所有節點的平衡因子,原因是:根據我們之前設計的調整策略調整之後,失衡子樹的高度不變,因此在最低層失衡節點之上的節點的平衡因子都不會受到影響,所以不必更改。

3. 確定節點B(B是A節點的孩子,根據S節點的大小與A節點的大小之間的關係確定是A的左孩子還是右孩子)

    根據A、B的平衡因子判斷是否失衡,如果失衡的話進一步判斷出失衡型別。

4. 根據失衡型別執行相應的調整策略。

5. 更新調整後從A到S路徑上各節點的平衡因子。

 

後記

之前在學這部分內容的時候基本上是靠背,一直不理解為什麼會分成這幾種型別以及為什麼這種型別就要按這樣的方法處理。我也一直沒有找到對此有解釋的資料,大部分書上和網上的資料都只是給出了結論,所以只好自己摸索,並將自己試圖理解的思路記錄下來和大家分析。

如果有紕漏還望指正。

 

參考書籍:《資料結構——C語言描述》耿國華著

 

相關文章