Wiki:在計算機科學中,AVL樹是最先被髮明的自平衡二叉查找樹。在AVL樹中,任一節點對應的兩棵子樹的最大高度差爲1,所以它也被稱爲高度平衡樹。查找、插入和刪除在平均和最壞狀況下的時間複雜度都是 O(logn)。增長和刪除元素的操做則可能須要藉由一次或屢次樹旋轉,以實現樹的從新平衡。AVL 樹得名於它的發明者 G. M. Adelson-Velsky 和 Evgenii Landis,他們在1962年的論文《An algorithm for the organization of information》中公開了這一數據結構。php
二叉搜索樹必定程度上能夠提升搜索效率,可是當原序列有序時,例如序列 A = {1,2,3,4,5,6},構造二叉搜索樹如圖 1.1。依據此序列構造的二叉搜索樹爲右斜樹,同時二叉樹退化成單鏈表,搜索效率下降爲 O(n)。node
在此二叉搜索樹中查找元素 6 須要查找 6 次。c++
二叉搜索樹的查找效率取決於樹的高度,所以保持樹的高度最小,便可保證樹的查找效率。一樣的序列 A,將其改成圖 1.2 的方式存儲,查找元素 6 時只需比較 3 次,查找效率提高一倍。數據結構
能夠看出當節點數目必定,保持樹的左右兩端保持平衡,樹的查找效率最高。函數
這種左右子樹的高度相差不超過 1 的樹爲平衡二叉樹。動畫
平衡二叉查找樹:簡稱平衡二叉樹。由前蘇聯的數學家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉樹,根據科學家的英文名也稱爲 AVL 樹。它具備以下幾個性質:this
平衡之意,如天平,即兩邊的份量大約相同。spa
例如圖 2.1 不是平衡二叉樹,由於節點 60 的左子樹不是平衡二叉樹。3d
圖 2.2 也不是平衡二叉樹,由於雖然任何一個節點的左子樹與右子樹都是平衡二叉樹,但高度之差已經超過 1 。指針
圖 2.3 是平衡二叉樹。
**定義:**某節點的左子樹與右子樹的高度(深度)差即爲該節點的平衡因子(BF,Balance Factor),平衡二叉樹中不存在平衡因子大於 1 的節點。在一棵平衡二叉樹中,節點的平衡因子只能取 0 、1 或者 -1 ,分別對應着左右子樹等高,左子樹比較高,右子樹比較高。
定義平衡二叉樹的節點結構:
typedef struct AVLNode *Tree;
typedef int ElementType;
struct AVLNode{
int depth; //深度,這裏計算每一個節點的深度,經過深度的比較可得出是否平衡
Tree parent; //該節點的父節點
ElementType val; //節點值
Tree lchild;
Tree rchild;
AVLNode(int val=0) {
parent = NULL;
depth = 0;
lchild = rchild = NULL;
this->val=val;
}
};
複製代碼
圖 5.1 是一顆平衡二叉樹
在此平衡二叉樹插入節點 99 ,樹結構變爲:
在動圖 5.2 中,節點 66 的左子樹高度爲 1,右子樹高度爲 3,此時平衡因子爲 -2,樹失去平衡。
在動圖 5.2 中,以節點 66 爲父節點的那顆樹就稱爲 最小失衡子樹。
最小失衡子樹:在新插入的節點向上查找,以第一個平衡因子的絕對值超過 1 的節點爲根的子樹稱爲最小不平衡子樹。也就是說,一棵失衡的樹,是有可能有多棵子樹同時失衡的。而這個時候,咱們只要調整最小的不平衡子樹,就可以將不平衡的樹調整爲平衡的樹。
平衡二叉樹的失衡調整主要是經過旋轉最小失衡子樹來實現的。根據旋轉的方向有兩種處理方式,左旋 與 右旋 。
旋轉的目的就是減小高度,經過下降整棵樹的高度來平衡。哪邊的樹高,就把那邊的樹向上旋轉。
以圖 5.1.1 爲例,加入新節點 99 後, 節點 66 的左子樹高度爲 1,右子樹高度爲 3,此時平衡因子爲 -2。爲保證樹的平衡,此時須要對節點 66 作出旋轉,由於右子樹高度高於左子樹,對節點進行左旋操做,流程以下:
(1)節點的右孩子替代此節點位置 (2)右孩子的左子樹變爲該節點的右子樹 (3)節點自己變爲右孩子的左子樹
整個操做流程如動圖 5.1.2 所示。
右旋操做與左旋相似,操做流程爲:
(1)節點的左孩子表明此節點 (2)節點的左孩子的右子樹變爲節點的左子樹 (3)將此節點做爲左孩子節點的右子樹。
假設一顆 AVL 樹的某個節點爲 A,有四種操做會使 A 的左右子樹高度差大於 1,從而破壞了原有 AVL 樹的平衡性。平衡二叉樹插入節點的狀況分爲如下四種:
具體分析以下:
只須要執行一次右旋便可。
實現代碼以下:
//LL型調整函數
//返回:新父節點
Tree LL_rotate(Tree node){
//node爲離操做節點最近的失衡的節點
Tree parent=NULL,son;
//獲取失衡節點的父節點
parent=node->parent;
//獲取失衡節點的左孩子
son=node->lchild;
//設置son節點右孩子的父指針
if (son->rchild!=NULL) son->rchild->parent=node;
//失衡節點的左孩子變動爲son的右孩子
node->lchild=son->rchild;
//更新失衡節點的高度信息
update_depth(node);
//失衡節點變成son的右孩子
son->rchild=node;
//設置son的父節點爲原失衡節點的父節點
son->parent=parent;
//若是失衡節點不是根節點,則開始更新父節點
if (parent!=NULL){
//若是父節點的左孩子是失衡節點,指向如今更新後的新孩子son
if (parent->lchild==node){
parent->lchild=son;
}else{
//父節點的右孩子是失衡節點
parent->rchild=son;
}
}
//設置失衡節點的父親
node->parent=son;
//更新son節點的高度信息
update_depth(son);
return son;
}
複製代碼
只須要執行一次左旋便可。
實現代碼以下:
//RR型調整函數
//返回新父節點
Tree RR_rotate(Tree node){
//node爲離操做節點最近的失衡的節點
Tree parent=NULL,son;
//獲取失衡節點的父節點
parent=node->parent;
//獲取失衡節點的右孩子
son=node->rchild;
//設置son節點左孩子的父指針
if (son->lchild!=NULL){
son->lchild->parent=node;
}
//失衡節點的右孩子變動爲son的左孩子
node->rchild=son->lchild;
//更新失衡節點的高度信息
update_depth(node);
//失衡節點變成son的左孩子
son->lchild=node;
//設置son的父節點爲原失衡節點的父節點
son->parent=parent;
//若是失衡節點不是根節點,則開始更新父節點
if (parent!=NULL){
//若是父節點的左孩子是失衡節點,指向如今更新後的新孩子son
if (parent->lchild==node){
parent->lchild=son;
}else{
//父節點的右孩子是失衡節點
parent->rchild=son;
}
}
//設置失衡節點的父親
node->parent=son;
//更新son節點的高度信息
update_depth(son);
return son;
}
複製代碼
若 A 的左孩子節點 B 的右子樹 E 插入節點 F ,致使節點 A 失衡,如圖:
A 的平衡因子爲 2 ,若仍按照右旋調整,則變化後的圖形爲這樣:
通過右旋調整發現,調整後樹仍然失衡,說明這種狀況單純的進行右旋操做不能使樹從新平衡。那麼這種插入方式須要執行兩步操做,使得旋轉以後爲 原來根節點的左孩子的右孩子做爲新的根節點。
(1)對失衡節點 A 的左孩子 B 進行左旋操做,即上述 RR 情形操做。 (2)對失衡節點 A 作右旋操做,即上述 LL 情形操做。
調整過程以下:
也就是說,通過這兩步操做,使得 原來根節點的左孩子的右孩子 E 節點成爲了新的根節點。
代碼實現:
//LR型,先左旋轉,再右旋轉
//返回:新父節點
Tree LR_rotate(Tree node){
RR_rotate(node->lchild);
return LL_rotate(node);
}
複製代碼
右孩子插入左節點的過程與左孩子插入右節點過程相似,也是須要執行兩步操做,使得旋轉以後爲 原來根節點的右孩子的左孩子做爲新的根節點。
(1)對失衡節點 A 的右孩子 C 進行右旋操做,即上述 LL 情形操做。 (2)對失衡節點 A 作左旋操做,即上述 RR 情形操做。
也就是說,通過這兩步操做,使得 原來根節點的右孩子的左孩子 D 節點成爲了新的根節點。
代碼實現:
//RL型,先右旋轉,再左旋轉
//返回:新父節點
Tree RL_rotate(Tree node){
LL_rotate(node->rchild);
return RR_rotate(node);
}
複製代碼
補充:
上述四種插入方式的代碼實現的輔助代碼以下:
//更新當前深度
void update_depth(Tree node){
if (node==NULL){
return;
}else{
int depth_Lchild=get_balance(node->lchild); //左孩子深度
int depth_Rchild=get_balance(node->rchild); //右孩子深度
node->depth=max(depth_Lchild,depth_Rchild)+1;
}
}
//獲取當前節點的深度
int get_balance(Tree node){
if (node==NULL){
return 0;
}
return node->depth;
}
//返回當前平衡因子
int is_balance(Tree node){
if (node==NULL){
return 0;
}else{
return get_balance(node->lchild)-get_balance(node->rchild);
}
}
複製代碼
AVL 樹和二叉查找樹的刪除操做狀況一致,都分爲四種狀況:
(1)刪除葉子節點 (2)刪除的節點只有左子樹 (3)刪除的節點只有右子樹 (4)刪除的節點既有左子樹又有右子樹
只不過 AVL 樹在刪除節點後須要從新檢查平衡性並修正,同時,刪除操做與插入操做後的平衡修正區別在於,插入操做後只須要對插入棧中的彈出的第一個非平衡節點進行修正,而刪除操做須要修正棧中的全部非平衡節點。
刪除操做的大體步驟以下:
對於刪除操做形成的非平衡狀態的修正,能夠這樣理解:對左或者右子樹的刪除操做至關於對右或者左子樹的插入操做,而後再對應上插入的四種狀況選擇相應的旋轉就行了。
處理步驟:
①、將該節點直接從樹中刪除;
②、其父節點的子樹高度的變化將致使父節點平衡因子的變化,經過向上檢索並推算其父節點是否失衡;
③、若是其父節點未失衡,則繼續向上檢索推算其父節點的父節點是否失衡...如此反覆②的判斷,直到根節點 ;若是向上推算過程當中發現了失衡的現象,則進行 ④ 的處理;
④、若是其父節點失衡,則判斷是哪一種失衡類型 [LL、LR、RR、RL] ,並對其進行相應的平衡化處理。若是平衡化處理結束後,發現與原來以父節點爲根節點的樹的高度發生變化,則繼續進行 ② 的檢索推算;若是與原來以父節點爲根節點的高度一致時,則可說明父節點的父節點及祖先節點的平衡因子將不會有變化,所以能夠退出處理;
具體數字演示:
處理步驟:
①、將左子樹(右子樹)替代原有節點 C 的位置;
②、節點 C 被刪除後,則以 C 的父節點 B 爲起始推算點,依此向上檢索推算各節點(父、祖先)是否失衡;
③、若是其父節點未失衡,則繼續向上檢索推算其父節點 的父節點 是否失衡...如此反覆 ② 的判斷,直到根節點 ;若是向上推算過程當中發現了失衡的現象,則進行 ④ 的處理;
④、若是其父節點失衡,則判斷是哪一種失衡類型 [LL、LR、RR、RL] ,並對其進行相應的平衡化處理。若是平衡化處理結束後,發現與原來以父節點爲根節點的樹的高度發生變化,則繼續進行 ② 的檢索推算;若是與原來以父節點爲根節點的高度一致時,則可說明父節點的父節點及祖先節點的平衡因子將不會有變化,所以能夠退出處理;
處理步驟:
①、找到被刪節點 B 和替代節點 BLR (節點 B 的前繼節點或後繼節點 —— 在此選擇 前繼);
②、將替代節點 BLR 的值賦給節點 B ,再把替代節點 BLR 的左孩子 BLRL 替換替代節點 BLR 的位置;
③、以 BLR 的父節點 BL 爲起始推算點,依此向上檢索推算父節點或祖先節點是否失衡;
④、若是其父節點未失衡,則繼續向上檢索推算其父節點的父節點是否失衡...如此反覆③的判斷,直到根節點;若是向上推算過程當中發現了失衡的現象,則進行⑤的處理;
⑤、若是其父節點失衡,則判斷是哪一種失衡類型 [LL、LR、RR、RL] ,並對其進行相應的平衡化處理。若是平衡化處理結束後,發現與原來以父節點爲根節點的樹的高度發生變化,則繼續進行 ② 的檢索推算;若是與原來以父節點爲根節點的高度一致時,則可說明父節點的父節點及祖先節點的平衡因子將不會有變化,所以能夠退出處理;
注:在這裏,小吳並無給出 AVL 的刪除操做的代碼,也沒有給出平衡性修復的動畫,由於我並不打算過多去討論它,更復雜的刪除操做過程將放在後續的 紅黑樹 中進行討論。
經過對 AVL 的插入操做和刪除操做能夠看出,平衡二叉樹的優點在於不會出現普通二叉查找樹的最差狀況,即退化成鏈表結構,但爲了保證高度平衡(對稱),動態插入和刪除的代價也隨之增長。
AVL 的旋轉問題看似複雜,但實際上若是你親自用筆紙操做一下仍是很好理解的。