數據結構與算法 -- 平衡二叉樹的構建
前言
上一篇學習了一些常見的靜態查找和動態查找中二叉搜索樹的查找,插入和刪除操作。在構建一個二叉搜索樹的時候,假如給定的數據是一直遞增的,那麼就會一直存儲在右子樹上,構成一個斜樹。這時在對其做查找時,效率一樣很低。
那麼在構建二叉搜索樹的時候,怎麼解決這樣問題呢?接下來我們介紹一下利用平衡二叉樹解決二叉搜索樹失衡的問題。
1. 平衡二叉樹構建分析
平衡二叉樹:是一種二叉排序樹,其中每一個結點的左右子樹的高度差至多等於1。平衡二叉樹又稱爲AVL樹。
高度平衡:要麼是一個空樹,要麼左右子樹都是平衡二叉樹。且左子樹和右子樹的深度差絕對值不超過1;我們將二叉樹上左子樹的深度減去右子樹的深度的值稱爲平衡因子BF
最小不平衡子樹:距離插入點最近的,且平衡因子的絕對值大於1的結點爲根的子樹,我們稱爲最小不平衡子樹。
如上圖中,當插入新節點37
時,就會打亂原有的平衡,圖中圈出來的就是最小不平衡子樹。
平衡二叉樹構建的基本思想:在構建二叉排序樹的過程中,每當插入新結點是,先檢查是否因插入而破壞了樹的平衡性,若是,則找到最小不平衡子樹,在保證二叉排序樹特性的前提下,調整最小不平衡子樹中各節點之間的鏈接關係,進行左旋或者右旋,使之成爲最新的平衡子樹。
接下來,我們舉個例子分析一下:
假設數組a[10] = {3,2,1,4,5,6,7,10,9,8}
。
在構建正常的二叉排序樹,如下:
顯然是失衡的,不能構成平衡二叉樹。
接下來,我們來模擬一下平衡二叉樹:
- 首先插入
結點 3,2,1
的過程。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LMz4zGHC-1590137686477)(https://user-gold-cdn.xitu.io/2020/5/22/1723a3da69fa1dbc?w=761&h=356&f=png&s=22368)]
當依次插入結點3、2、1
後,結點3
的平衡因子是2
,進行右旋。
- 插入
結點 4
插入結點4
後,沒有失衡
- 插入
結點 5
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-0au4hsI3-1590137686492)(https://user-gold-cdn.xitu.io/2020/5/22/1723a53e8c4c4296?w=867&h=408&f=png&s=28139)]
當插入結點5
時,結點3
的平衡因子爲-2
,成爲最小不平衡子樹,進行左旋。
- 插入
結點 6
當插入結點6
時,結點2
的平衡因子爲-2
,成爲最小不平衡子樹,進行左旋。
- 插入
結點7
結點5
的平衡因子爲-2
,成爲最小不平衡子樹,進行左旋。
- 插入
結點10
插入後沒有破壞原來的平衡
- 插入
結點9
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oR5oJgVb-1590137686503)(https://user-gold-cdn.xitu.io/2020/5/22/1723a5a4ca9c683d?w=532&h=372&f=png&s=46088)]
結點7
的平衡因子爲-2
,成爲最小不平衡子樹,進行左旋。此時結點9
比結點10
小,不能成爲結點10
的右子樹。
那麼應該怎麼辦呢?
那麼我們先總結一下上面的規律:
1. 平衡因子爲負數,左旋
2. 平衡因子爲正數,右旋
3. 平衡因子爲有正負數,先旋轉統一平衡因子的符號,進行雙旋轉
那麼,我們可以先對結點10
和結點9
,進行一次右旋,如下:
此時,平衡因子的符號統一,結點7
的平衡因子
爲-2
,然後再進行一次左旋,如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IQ60Oawh-1590137686508)(https://user-gold-cdn.xitu.io/2020/5/22/1723a65a2250fc8a?w=560&h=362&f=png&s=46322)]
- 插入
結點8
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-r8FqclrC-1590137686510)(https://user-gold-cdn.xitu.io/2020/5/22/1723a66db2414c67?w=555&h=434&f=png&s=60500)]
當插入結點8
時,出現了平衡因子符號不統一的情況,先進行右旋統一符號,得到如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-n0dq8cqg-1590137686512)(https://user-gold-cdn.xitu.io/2020/5/22/1723a68ae34a55ef?w=618&h=445&f=png&s=63653)]
然後對結點6
,進行左旋。
最終數組a插入完畢,得到平衡二叉樹。
旋轉規律:
- 平衡因子爲負數,左旋
- 平衡因子爲正數,右旋
- 平衡因子爲有正負數,先旋轉統一平衡因子的符號,進行雙旋轉
2. 平衡二叉樹構建
經過前面對平衡二叉樹構建的分析,接下來分析一下具體怎麼構建平衡二叉樹。
首先,我們對二叉樹
的二叉鏈表結點
結構,進行改造,加入平衡因子
屬性。
二叉樹
的二叉鏈表結點
結構如下:
//二叉樹的二叉鏈表結點結構定義
//結點結構
typedef struct BiTNode{
//結點數據
int data;
//結點的平衡因子
int bf;
//結點左右孩子指針
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
當平衡二叉樹節點需要右旋操作時:
如上圖,插入節點N
時,破壞了原有的平衡,需要進行右旋,步驟如下:
1. 將 P 做爲右旋的根節點
2. L 的右子樹,即:Lr,成爲 P 的左子樹
3. P 成爲 L 的右子樹
4. L 替換原來的P,成爲二叉排序樹新的根節點
右旋的實現:
// 右旋
void R_Rotate(BiTree *p){
BiTree L;
//✅ L是p的左子樹;
L = (*p)->lchild;
//✅ L的右子樹作爲p的左子樹
(*p)->lchild = L->rchild;
//✅ 將p作爲L的右子樹
L->rchild = (*p);
//✅ 將L替換原有p的根結點位置
*p = L;
}
當平衡二叉樹節點需要左旋操作時,其實和右旋是一樣的思路:
1. P 作爲左旋的根節點
2. R 的左子樹,成爲P的右子樹
3. P成爲R的左子樹
4. R 替換 P 成爲二叉樹新的根節點
左旋代碼實現:
// 左旋
void L_Rotate(BiTree *p){
BiTree R;
//✅ R是p的右子樹
R = (*p)->rchild;
//✅ R的左子樹作爲R的右子樹
(*p)->rchild = R->lchild;
//✅ 將p作爲R的左子樹;
R->lchild = (*p);
//✅ 將R替換原有p的根結點的位置
*p = R;
}
當需要雙旋操作時,又分爲兩種情況,分別是左子樹失衡需要雙旋和右子樹失衡需要雙旋,我們可以先定義三個常量:
#define LH +1 /* 左高 */
#define EH 0 /* 等高 */
#define RH -1 /* 右高 */
-
左子樹失衡需要雙旋:
雙旋前要先統一平衡因子的符號,然後先左旋,再右旋
-
右子樹失衡需要雙旋
雙旋前要先統一平衡因子的符號,然後先右旋,再左旋
那麼接下來看一下整體的平衡二叉樹(AVL樹) 的實現:
在平衡二叉排序樹T中,不存在和插入元素e有相同關鍵字的結點,則插入一個數據元素爲e的新節點,並返回1,否則返回0。若因爲插入而是二叉排序樹失衡,則做平衡旋轉處理。變量taller反映 平衡二叉排序樹T 是否長高。
構建思路:
-
- 判斷 二叉排序樹 T,是否爲空,爲空則創建一個新節點,此時,只有一個結點,其平衡因子BF爲EH,表示等高,新結點,默認長高,
taller = TRUE
- 判斷 二叉排序樹 T,是否爲空,爲空則創建一個新節點,此時,只有一個結點,其平衡因子BF爲EH,表示等高,新結點,默認長高,
-
- T 不爲空,判斷插入元素e是否等於結點T的數據,相等則返回
false
,默認taller = false
,不長高
- T 不爲空,判斷插入元素e是否等於結點T的數據,相等則返回
-
- 當插入元素e小於結點T的數據時,在T的左子樹中進行搜索:
-
3.1 先遞歸判斷是否成功插入,未插入,直接返回
false
-
3.2 判斷成功插入到T的左子樹中,且左子樹長高時:
-
3.3 檢查T的平衡度:
- 當T的BF爲
LH(左高)
時,原本左子樹比右子樹高,需要做左平衡處理,此時taller = false
- 當T的BF爲
EH(等高)
時,原本左子樹和右子樹等高,左子樹增高而使樹增高,此時taller = true
,T的BF爲LH
- 當T的BF爲
RH(右高)
時,原本右子樹比左子樹高,左子樹增高而使左右子樹等高,此時taller = false
,T的BF爲EH
- 當T的BF爲
-
- 當插入元素e大於結點T的數據時,在T的右子樹中進行搜索:
-
4.1 先遞歸判斷是否成功插入,未插入,直接返回
false
-
4.2 判斷成功插入到T的左子樹中,且右子樹長高時:
-
4.3 檢查T的平衡度:
- 當T的BF爲
LH(左高)
時,原本左子樹比右子樹高,右子樹增高而使左右子樹等高,此時taller = false
,T的BF爲EH
。 - 當T的BF爲
EH(等高)
時,原本左子樹和右子樹等高,右子樹增高而使樹增高,此時taller = true
,T的BF爲RH
。 - 當T的BF爲
RH(右高)
時,原本右子樹比左子樹高,右子樹增高而使樹失衡,需要做右平衡處理,此時taller = false
。
- 當T的BF爲
左平衡處理思路:
-
檢查T的左子樹的平衡度
L = T->lchild
,若其左子樹L
的平衡度爲LH
,新結點插入在T的左孩子L的左子樹上,要作單右旋處理(右旋上面已經分析過) -
T的左子樹的平衡度爲
RH
,即:L->bf = RH
時,需要雙旋處理,Lr
指向T的左孩子的右子樹根,即:Lr=L->rchild
,修改T
及其左孩子
的平衡因子- 當
Lr
的BF
爲LH
(左高)時,第一次左旋後,L平衡
,修改其BF
爲EH(等高)
,由於T
之前是平衡的,進行旋轉後修改T
的BF
爲RH
。 - 當
Lr
的BF
爲EH
(等高)時,第一次左旋後,L平衡
,修改其BF
爲EH(等高)
,修改T
的BF
爲EH
。 - 當
Lr
的BF
爲RH
(右高)時,第一次左旋後,L平衡
,修改其BF
爲LH(左高)
,修改T
的BF
爲EH
。
- 當
-
修改
Lr
的BF
爲EH
-
對
T的左子樹
做左旋處理 -
對
T
做右旋處理
右平衡處理思路:
-
檢查T的右子樹的平衡度
R = T->rchild
,若其右子樹R
的平衡度爲RH
,新結點插入在T的右孩子R的右子樹上,要作單左旋處理(左旋上面已經分析過) -
T的右子樹的平衡度爲
LH
,即:R->bf = LH
時,需要雙旋處理,Rl
指向T的右孩子的左子樹,即:Rl=R->lchild
,修改T
及其右孩子
的平衡因子- 當
Rl
的BF
爲LH
(左高)時,第一次右旋後,R平衡
,修改其BF
爲RH(等高)
,由於T
之前是平衡的,進行旋轉後修改T
的BF
爲EH
。 - 當
Rl
的BF
爲EH
(等高)時,第一次右旋後,R平衡
,修改其BF
爲EH(等高)
,修改T
的BF
爲EH
。 - 當
Rl
的BF
爲RH
(右高)時,第一次右旋後,R平衡
,修改其BF
爲EH(等高)
,修改T
的BF
爲LH
。
- 當
-
修改
Rl
的BF
爲EH
-
對
T的右子樹
做右旋處理 -
對
T
做左旋處理
Status InsertAVL(BiTree *T,int e,Status *taller)
{
if(!*T)
{ //1.插入新結點,樹“長高”,置taller爲TRUE
//① 開闢一個新結點T;
*T=(BiTree)malloc(sizeof(BiTNode));
//② 對新結點T的data賦值,並且讓其左右孩子指向爲空,T的BF值爲EH;
(*T)->data=e;
(*T)->lchild=(*T)->rchild=NULL;
(*T)->bf=EH;
//③ 新結點默認"長高"
*taller=TRUE;
}
else
{
if (e==(*T)->data)
{ //2.樹中已存在和e有相同關鍵字的結點則不再插入
*taller=FALSE;
return FALSE;
}
if (e<(*T)->data)
{
//3.應繼續在T的左子樹中進行搜索
if(!InsertAVL(&(*T)->lchild,e,taller))
//未插入
return FALSE;
//4.已插入到T的左子樹中且左子樹“長高”
if(*taller)
//5.檢查T的平衡度
switch((*T)->bf)
{
case LH:
//原本左子樹比右子樹高,需要作左平衡處理
LeftBalance(T);
*taller=FALSE;
break;
case EH:
//原本左、右子樹等高,現因左子樹增高而使樹增高
(*T)->bf=LH;
*taller=TRUE;
break;
case RH:
//原本右子樹比左子樹高,現左、右子樹等高
(*T)->bf=EH;
*taller=FALSE;
break;
}
}
else
{ //6.應繼續在T的右子樹中進行搜索
//未插入
if(!InsertAVL(&(*T)->rchild,e,taller))
return FALSE;
//已插入到T的右子樹且右子樹“長高”
if(*taller)
// 檢查T的平衡度
switch((*T)->bf)
{
//原本左子樹比右子樹高,現左、右子樹等高
case LH:
(*T)->bf=EH;
*taller=FALSE;
break;
//原本左、右子樹等高,現因右子樹增高而使樹增高
case EH:
(*T)->bf=RH;
*taller=TRUE;
break;
// 原本右子樹比左子樹高,需要作右平衡處理
case RH:
RightBalance(T);
*taller=FALSE;
break;
}
}
}
return TRUE;
}
// 左平衡樹失衡處理
void LeftBalance(BiTree *T)
{
BiTree L,Lr;
//1.L指向T的左子樹根結點
L=(*T)->lchild;
//2.檢查T的左子樹的平衡度,並作相應平衡處理
switch(L->bf)
{
//① 新結點插入在T的左孩子的左子樹上,要作單右旋處理(如圖1-平衡二叉樹右旋解釋圖)
case LH:
//L的平衡因子爲LH,即爲1時,表示它與根結點BF符合相同,則將它們(T,L)的BF值都改爲EH(0)
(*T)->bf=L->bf=EH;
//對最小不平衡子樹T進行右旋;
R_Rotate(T);
break;
//② LH的平衡因子爲RH(-1)時,它與跟結點的BF值符合相反.此時需要做雙旋處理(2次旋轉處理)
// 新結點插入在T的左孩子的右子樹上,要作 雙旋處理
case RH:
//Lr指向T的左孩子的右子樹根
Lr=L->rchild;
//修改T及其左孩子的平衡因子
switch(Lr->bf)
{
case LH:
(*T)->bf=RH;
L->bf=EH;
break;
case EH:
(*T)->bf=L->bf=EH;
break;
case RH:
(*T)->bf=EH;
L->bf=LH;
break;
}
Lr->bf=EH;
//對T的左子樹作左旋平衡處理
L_Rotate(&(*T)->lchild);
//對T作右旋平衡處理
R_Rotate(T);
}
}
// 右平衡樹失衡處理
void RightBalance(BiTree *T)
{
BiTree R,Rl;
//1.R指向T的右子樹根結點
R=(*T)->rchild;
//2. 檢查T的右子樹的平衡度,並作相應平衡處理
switch(R->bf)
{
//① 新結點插入在T的右孩子的右子樹上,要作單左旋處理
case RH:
(*T)->bf=R->bf=EH;
L_Rotate(T);
break;
//新結點插入在T的右孩子的左子樹上,要作雙旋處理
case LH:
//Rl指向T的右孩子的左子樹根
Rl=R->lchild;
//修改T及其右孩子的平衡因子
switch(Rl->bf)
{
case RH:
(*T)->bf=LH;
R->bf=EH;
break;
case EH:
(*T)->bf=R->bf=EH;
break;
case LH:
(*T)->bf=EH;
R->bf=RH;
break;
}
Rl->bf=EH;
//對T的右子樹作右旋平衡處理
R_Rotate(&(*T)->rchild);
//對T作左旋平衡處理
L_Rotate(T);
}
}
個人見解,如有錯誤,歡迎指正。