查詢演算法 | 平衡二叉樹(AVL樹)詳細分析
AVL:完全平衡的二叉查詢樹
二叉查詢樹可以表示動態的資料集合,對於給定的資料集合,在建立一顆二叉查詢樹時,二叉查詢樹的結構形態與關鍵字的插入順序有關。如果全部或者部分地按照關鍵字的遞增或者遞減順序插入二叉查詢樹的結點,則所建立的二叉查詢樹全部或者在區域性形成退化的單分支結構。在最壞的情況下,二叉查詢樹可能完全偏斜,高度為n
,其平均與最壞的情況下查詢時間都是O(n)
;而最好的情況下,二叉查詢樹的結點儘可能靠近根結點,其平均與最好情況的查詢時間都是O(logn)。因此,我們希望最理想的狀態下是使二叉查詢樹始終處於良好的結構形態。(圖片有點問題,意會就好)
1962年,Adelson-Velsikii和Landis提出了一種結點在高度上相對平衡的二叉查詢樹,又稱為AVL樹。其平均和最壞情況下的查詢時間都是O(logn)
插入和刪除的時間複雜性也會保持O(logn)
,且在插入和刪除之後,在高度上仍然保持平衡。
AVL樹
又稱為平衡二叉樹
,即Balanced Binary Tree或者Height-Balanced Tree,它或者是一棵空二叉樹,或者是具有如下性質的二叉查詢樹:
- 左子樹和右子樹都是高度平衡的二叉樹;
- 左子樹和右子樹的高度之差的絕對值不超過1。
如果將二叉樹上結點的平衡因子
BF(Balanced Factor)定義為該結點的左子樹與右子樹的高度之差,根據AVL樹的定義,AVL樹中的任意結點的平衡因子只可能是-1(右子樹高於左子樹)、0或者1(左子樹高於右子樹),在某些圖中也會表示為絕對高度差,即0,1,2這種形式,請注意理解。
BalanceFactor = height(left-sutree) − height(right-sutree)
Rebalance:平衡調整
AVL樹的調整過程很類似於數學歸納法,每次在插入新節點之後都會找到離新插入節點最近的非平衡葉節點,然後對其進行旋轉操作以使得樹中的每個節點都處於平衡狀態。
Left Rotation:左旋,右子樹右子節點
當新插入的結點為右子樹的右子結點時,我們需要進行左旋操作來保證此部分子樹繼續處於平衡狀態。
我們應該找到離新插入的結點最近的一個非平衡結點,來以其為軸進行旋轉,下面看一個比較複雜的情況:
Right Rotation:右旋,左子樹左子節點
當新插入的結點為左子樹的左子結點時,我們需要進行右旋操作來保證此部分子樹繼續處於平衡狀態。
下面看一個比較複雜的情況:
Left-Right Rotation:先左旋再右旋,左子樹右子節點
在某些情況下我們需要進行兩次旋轉操作,譬如在如下的情況下,某個結點被插入到了左子樹的右子結點:
我們首先要以A為軸進行左旋操作,然後需要以C為軸進行右旋操作,最終得到的又是一棵平衡樹。
下面看一個比較複雜的情況:
注意
:這裡還有另外一種情況沒有畫出來,就是新插入的節點為45的左孩子。大家自己拿筆和紙畫一畫。
Right-Left Rotation:先右旋再左旋,右子樹左子節點
注意:這裡還有另外一種情況沒有畫出來,就是新插入的節點為55的右孩子。大家自己拿筆和紙畫一畫。
構建平衡二叉樹的程式碼實現
#include <stdio.h>
#include <stdlib.h>
//分別定義平衡因子數
#define LH +1
#define EH 0
#define RH -1
typedef int ElemType;
typedef enum {false,true} bool;
//定義二叉排序樹
typedef struct BSTNode{
ElemType data; //節點儲存的資料
int bf; //平衡標誌
struct BSTNode *lchild,*rchild;
}*BSTree,BSTNode;
//對以 p 為根結點(以 p 為軸)的二叉樹做右旋處理,令 p 指標指向新的樹根結點
void R_Rotate(BSTree* p)
{
// 令 p 的左孩子的右孩子作為 p 的左孩子
BSTree lc = (*p)->lchild;
(*p)->lchild = lc->rchild;
//令 p 作為 p 的左孩子的右孩子
lc->rchild = *p;
// 令 p 的左孩子作為新的根節點
*p = lc;
}
//對以 p 為根結點(以 p 為軸)的二叉樹做左旋處理,令 p 指標指向新的樹根結點
void L_Rotate(BSTree* p)
{
// 令 p 的右孩子的左孩子作為 p 的右孩子
BSTree rc = (*p)->rchild;
(*p)->rchild = rc->lchild;
//令 p 作為 p 的右孩子的左孩子
rc->lchild = *p;
// 令 p 的右孩子作為新的根節點
*p = rc;
}
//對以指標 T 所指向結點為根結點的二叉樹作左子樹的平衡處理,令指標 T 指向新的根結點
void LeftBalance(BSTree* T)
{
BSTree lc,rd;
lc = (*T)->lchild;
//進入此方法前就已經判斷為左旋了,還需要確定是哪種左旋:1、直接左旋,2、先左旋後右旋
//檢視以 T 的左子樹為根結點的子樹,失去平衡的原因:
// 如果 bf 值為 1 ,則說明新增在左子樹為根結點的左子樹中,需要對其進行右旋處理;
// 反之,如果 bf 值為 -1,說明新增在以左子樹為根結點的右子樹中,需要進行雙向先左旋後右旋的處理
switch (lc->bf)
{
case LH:
(*T)->bf = lc->bf = EH;
R_Rotate(T);
break;
case RH:
rd = lc->rchild;
switch(rd->bf) //這部分不好理解,拿筆在紙上畫一畫就特別好理解了(這部分的情況在前面的講解裡沒有)
{
case LH:
(*T)->bf = RH;
lc->bf = EH;
break;
case EH:
(*T)->bf = lc->bf = EH;
break;
case RH:
(*T)->bf = EH;
lc->bf = LH;
break;
}
rd->bf = EH;
L_Rotate(&(*T)->lchild);
R_Rotate(T);
break;
}
}
//右子樹的平衡處理同左子樹的平衡處理完全類似
void RightBalance(BSTree* T)
{
BSTree lc,rd;
lc= (*T)->rchild;
switch (lc->bf)
{
case RH:
(*T)->bf = lc->bf = EH;
L_Rotate(T);
break;
case LH:
rd = lc->lchild;
switch(rd->bf)
{
case LH:
(*T)->bf = EH;
lc->bf = RH;
break;
case EH:
(*T)->bf = lc->bf = EH;
break;
case RH:
(*T)->bf = EH;
lc->bf = LH;
break;
}
rd->bf = EH;
R_Rotate(&(*T)->rchild);
L_Rotate(T);
break;
}
}
//taller 用於記錄插入新節點後是否使相應的子樹高度增加,從而根據平衡因子判斷是否需要対樹進行平衡
//演算法使用了遞迴,插入完成後,逐層向上對相應子樹進行處理
//無返回值的地方,統一在最後 return 1;
int InsertAVL(BSTree* T,ElemType e,bool* taller)
{
//如果本身為空樹(樹的根節點或相應子樹為空),則直接新增 e 為根結點
if ((*T)==NULL)
{
(*T)=(BSTree)malloc(sizeof(BSTNode));
(*T)->bf = EH;
(*T)->data = e;
(*T)->lchild = NULL;
(*T)->rchild = NULL;
*taller=true;
}
//如果二叉排序樹中已經存在 e ,則不做任何處理
else if (e == (*T)->data)
{
*taller = false;
return 0;
}
//如果 e 小於結點 T 的資料域,則插入到 T 的左子樹中
else if (e < (*T)->data)
{
//如果插入過程,不會影響樹本身的平衡,則直接結束
if(!InsertAVL(&(*T)->lchild,e,taller))
return 0;
//判斷插入過程是否會導致整棵樹的深度 +1
if(*taller)
{
//判斷根結點 T 的平衡因子是多少,由於是在其左子樹新增新結點的過程中導致失去平衡,
//所以當 T 結點的平衡因子本身為 1(左高)時,需要進行左子樹的平衡處理,否則更新樹中各結點的平衡因子數
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;
}
}
}
//同樣,當 e>T->data 時,需要插入到以 T 為根結點的樹的右子樹中,同樣需要做和以上同樣的操作
else
{
if(!InsertAVL(&(*T)->rchild,e,taller))
return 0;
if (*taller)
{
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 1;
}
//判斷現有平衡二叉樹中是否已經具有資料域為 e 的結點
bool FindNode(BSTree root,ElemType e,BSTree* pos)
{
BSTree pt = root;
(*pos) = NULL;
while(pt)
{
if (pt->data == e)
{
//找到節點,pos指向該節點並返回true
(*pos) = pt;
return true;
}
else if (pt->data>e)
{
pt = pt->lchild;
}
else
pt = pt->rchild;
}
return false;
}
//中序遍歷平衡二叉樹
void InorderTra(BSTree root)
{
if(root->lchild)
InorderTra(root->lchild);
printf("%d ",root->data);
if(root->rchild)
InorderTra(root->rchild);
}
int main()
{
int i,nArr[] = {1,23,45,34,98,9,4,35,23};
BSTree root=NULL,pos;
bool taller;
//用 nArr查詢表構建平衡二叉樹(不斷插入資料的過程)
for (i=0;i<9;i++)
{
InsertAVL(&root,nArr[i],&taller);
}
//中序遍歷輸出
InorderTra(root);
//判斷平衡二叉樹中是否含有資料域為 103 的資料
if(FindNode(root,103,&pos))
printf("\n%d\n",pos->data);
else
printf("\nNot find this Node\n");
return 0;
}