二叉排序樹
又稱爲二叉查找樹。它或者是一棵空樹,或者是具有下列性質的二叉樹,
1. 若它的左子樹不空,則左子樹上的所有結點的值均小於它的根結構值
2. 若它的右子樹不空,則右子樹上的所有結點的值均大於它的根結點值
3. 它的左、右子樹也分別爲二叉排序樹。
二叉排序樹查找操作
首先我們提供一個二叉樹的結構
typedef struct BiTNode
{
int data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree
然後我們來看一下二叉排序樹的查找是如何實現的
/*
遞歸查找二叉排序樹T中是否存在key,指針f指向T的雙親,其初始調用值爲NULL
若查找成功,則指針p指向該數據元素結點,並返回TRUE
否則指針p指向查找路徑上訪問的最後一個結點並返回FALSE
*/
Status SearchBST(BiTree T,int key,BiTree f,BiTree *p)
{
if(!T)
{
*p=f;
return FALSE;
}
else if(key==T->data)
{
*p = T
return TRUE;
}
else if(key<T->data)
{
return SearchBST(T->lchild,key,T,p); //在左子樹中查找
}
else
return SearchBST(T->lchild,key,T,p); //在右子樹中查找
}
二叉排序樹插入操作
有了二叉排序樹的查找函數,那麼所謂的二叉排序樹的插入,其實也就是將關鍵字放到樹中的合適位置。代碼如下
/*
當二叉排序樹T中不存在關鍵字等於key的數據元素時,插入key並返回TRUE,否則返回FLASE
*/
Status InsertBST(BiTree *T,int key)
{
BiTree p,s
if(!SearchBST(*T,key,NULL,&p))/*查找不成功*/
{
s =(BiTree)malloc(sizeof(BiTNode));
s->data = key;
s->lchild=s->rchild=NULL;
if(!p)
*T = s; //插入s爲新的根結點
else if(key<p->data)
p->lchild = s;
else
p->rchild = s;
return TRUE;
}else
return FALSE;
}
有了二叉排序樹的插入代碼,我們要實現二叉排序樹的構建非常容易了,如下代碼
int i
int a[10] = {62,88,58,47,35,73,51,99,37,93};
BiTree T=NULL;
for(i=0;i<10;i++)
{
InsertBST(&T,a[i]);
}
二叉排序樹的刪除操作
俗話說“請神容易送神難”,我們已經介紹了二叉排序樹的查找與插入算法,但是對於二叉排序樹的刪除,就不是那麼容易,我們不能因爲刪除了結點,而讓這棵樹變得不滿足二叉排序樹的特性,所以刪除需要考慮多種情況,刪除葉子結點沒有問題,因爲對整個樹來說,其他結點的結構並未受到影響。
刪除只有左子樹或只有右子樹的情況,相對也比較好解決,那就是結點刪除後,將它的左子樹或右子樹整個移動到刪除結點的位置即可,可以理解爲獨子繼承父業。
但是對於要刪除的結點既有左子樹又有右子樹的情況怎麼辦呢?
上圖這種重新挨個插入效率不高且不說,還會導致整個二叉排序樹結構發生很大的變化,有可能會增加樹的調試。增加調試可不是個好事,這我們待會再說。
我們仔細觀察一下,47的兩個子樹中能否找出一個結點可以代替47呢?果然有,37或者48都可以代替47,此時在刪除47後,整個二叉排序樹並沒有必生本質的改變。
爲什麼是37和48?對的,它們正好是二叉排序樹中比它小或比它大的最接近47的兩個數。也就是說,如果我們對這棵二叉排序樹進行中序遍歷,得到的序列
{25,35,36,37,47,48,49,50,51,56,58,62,73,88,93,99}
它們正好是47的前驅和後繼。
因此比較好的辦法就是,找到需要刪除的結點p的直接前驅(或直接後繼)s,用s來替換結點p,然後再刪除此結點s
根據分析 有三種情況:
n 葉子結點
n 僅有左或右子樹的結點
n 左右子樹都有的結點,我們來看代碼,下面這個算法是遞歸方式對二叉排序樹T查找key,查找到時刪除
/*
若二叉排序樹T中存在關鍵字等於key的數據元素時,則刪除該數據元素結點
並返回TRUE,否則返回FALSE
*/
Status DeleteBST(BiTree *T,int key)
{
if(!*T) //不存在關鍵字等於key的數據元素
return FALSE;
else
{
if(key==(*T)->data)
return Delete(T);
else if(key<(*T)->data)
return DeleteBST(&(*T)->lchild,key);
else
return DeleteBST(&(&T)->rchild,key);
}
}
/*從二叉排序樹中刪除結點p,並重接它左或右子樹*/
Status Delete(BiTree *p)
{
BiTree q,s;
if((*p)->rchild==NULL) //重接左子樹
{
q=(*p);*p=(*p)-lchild;free(q);
}
else if((*p)->lchild==NULL)
{
q=*p;*p=(*p)->rchild;free(q);
}
else //左右子樹都不爲空
{
q=*p;s=(*p)-lchild;
while(s->rchild)
{
q=s;s=s->rchild; /*轉左,然後向右到盡頭*/
}
(*p)->data=s->data; //s指向被刪結點的直接前驅
if(q!=*p)
q->rchild=s->lchild;//重接q右子樹
else
q->lchild=s->lchild; //重接q左子樹
free(s);
}
return TRUE;
}
1. 程序開始執行,代碼4~7行的目的是爲了刪除沒有右子樹只有左子樹的結點。此時只需將此結點的左孩子替換它自己,然後釋放此結點內存,就等於刪除了。
2. 代碼第8~11行是同樣的道理處理只有右子樹沒有左子樹的結點刪除問題。
3. 第12~25行處理複雜的左右子樹均存在的問題
4. 第14行,將要刪除的結點p賦值給臨時的變量q,再將p的左孩子p->lchild賦值給臨時的變量s。此時q指向47結點,s指向35結點。如圖
5. 第15~18行,循環找到左子樹的右結點,直到右側盡頭。就當前例子來說,就讓q指向35,而s指向37這個沒有右子樹的結點,如圖
6. 第19行,此時要刪除結點p的位置的數據被賦值爲s->data,即讓p->data=37,如圖
7. 第20~23行,如果p和q指向不同,則將s->lchild賦值給q->rchild,否則就是將s->lchild賦值給q->lchild。顯然這個例子p不等於q,將s->lchild指向的36賦值給q->rchild,也就是讓q->rchild指向36結點,如圖
8. 第24行,free(s),就非常好理解了,將37結點刪除,
從這段代碼也可以看出,我們其實是在找刪除結點的前驅結點替換的方法,對於後繼結點來替換,方法上是一樣的
二叉排序樹總結
二叉排序樹是以樹是以鏈接的方式存儲,保持了鏈接存儲結構在執行插入或刪除操作時不用移動元素的優點,只要找到合適的插入和刪除位置後,僅需要修改鏈接指針即可。插入刪除的時間性能比較好。而對於二叉排序樹的查找,走的就是人根結點到要查找的結點的路徑,其比較次數等於給定值的結點在二叉樹的層數。極端情況,最少爲1次,最多也不超過樹的深度。也就說,二叉排序樹的查找性能取決於二叉排序樹的形狀。
如圖
{62,88,58,47,35,73,51,99,37,93}這樣的數組,我可以表示爲上圖左圖的二叉排序樹。
但如果數組元素的次序是從小到大的有序,如
{35,37,47,51,58,62,73,88,93,99},則二叉排序樹就成了極端的右斜樹,注意它依然是一棵二叉排序樹,如上圖右。同樣去找99。左圖比較兩次,右圖要比較10次。二者差異很大的
也就是說,我們希望二叉排序樹是比較平衡的,即其深度與完全二叉樹相同,均爲,那麼查找時間複雜度也就是O(logn),近似折半查找,不平衡的最壞情況就像上圖右的斜樹,查找時間複雜度爲O(n),這等同於順序查找。因此,如果我們希望對一個集合按二叉排序樹查找,最好是把它構建成一棵平衡的二叉排序樹。看下面
平衡二叉樹(AVL樹)
平衡二叉樹(Self-Blancing Binary Search Tree 或 Height-Blanced Binary Search Tree)是一種二叉排序樹,其中每個節點的左子樹和右子樹的高度差至多等於1。
我們將二叉樹上結點的左子樹深度減去右子樹深度的值稱爲平衡因子BF(Balance Factor)
那麼平衡二叉樹上所有結點的平衡因子只可能是-1、0、1。只要二叉樹上所有結點的平衡因子的絕對值大於1,則該二叉樹就是不平衡的。如圖
爲什麼上圖1是平衡二叉樹,圖2不是?這就考查我們對平衡二叉樹的定義的理解,它的前提首先是一棵二叉排序樹。
圖3不是的原因是,結點左子樹高度是3,右是0,差值大於1啦。不符合定義。
距離插入結點最近的,且平衡因子的絕對值大於1的結點爲根的子樹,我們稱爲最小不平衡樹。如圖
新插入結點37時,距離它最近的平衡因子絕對值超過1的結點是58,所以從58開始以下的子樹爲最小不平衡子樹。
平衡二叉樹實現原理
平衡二叉樹構建的基本思想就是在構建二叉排序 樹的過程中,每當插入一個結點時,先檢查是否因插入而破壞了樹的平衡性,若是,則找出最小不平衡子樹。在保持二叉排序樹特性的前提下,調整最小不平衡子樹中各結點之間的鏈接關係,進行相應的旋轉,使之成爲新的平衡子樹。
爲了在學習算法時能夠輕鬆一些,我們先講一個平衡二叉樹的構建過程。如
{3,2,1,4,5,6,7,10,9,8}需要構建二叉排序樹。在沒有學習平衡二叉樹之前,根據二叉排序樹的特性,我們通常會將它構建成下圖左
雖然符合二叉排序樹的定義,但是對這樣的高度達到8的二叉樹來說,查找是非常不利的。我們更期望能構建右圖那的二叉排序樹。
對於數組{3,2,1,4,5,6,7,10,9,8}的前兩們3和2,我們很正常地構建,到了第3個數時“1”時,發現此時根結點“3”的平衡因子變成了2,此時整棵樹都成了最小不平不平衡子樹,因此需要調整,如圖,結點左上角的數字爲平衡因子BF值。因爲BF值爲正,因此我們將整個樹進行右旋(順時針旋轉),此時結點2成了根結點,3成了2的右孩子這樣三個結點的BF值均爲0。非常平衡如圖2
然後我們再增加結點4,平衡因子沒有超出限定範圍,如圖3
增加結點5時,結點3的BF爲-2說明要旋轉了。由於BF是負值,所以我們對這棵最小不平衡子樹進行左旋(逆時針旋轉)圖4
繼續,增加結點6時,發現根結點2的BF值變成了-2,如下圖6。所以對根結點進行了左旋,注意此時本來結點3是4的左孩子,由於旋轉後需要滿足二叉排序樹特性,因此它成了結點2的右孩子,如圖7。增加結點7, 同樣的左旋轉,使得整棵樹達到平衡,如圖8,圖9
當增加結點10時,結構無變化,如圖10。
再增加結點9,此時結點7的BF的變成了-2,理論上我們只需要旋轉最小不平衡子樹7、9、10即可,但是如果左旋轉後,結點9就成了10的右孩子,這不符合二叉排序樹的特性的。此時不能簡單的左旋。
仔細觀察圖11,發現根本原因在於結點7的BF是-2,而結點10的BF是1,也就是說,它們倆一正一負,符號並不統一,而前面幾次旋轉,無論左還是右旋,最小不平衡樹的根結點與它的子結點符號都是相同的。這就是不能直接旋轉的關鍵。
不統一,就將其統一。於是我們對結點9和結點10進行右旋,使得結點10成爲9的右子樹,結點9的BF爲-1,此時就與結點7的BF值符號統一了,如圖12
這樣我們再以結點7爲最小不平衡樹進行左旋,得到圖13。
接着插入8,情況與賜教類似,結點6的BF是-2,而它的右孩子9的BF是1,因此首先以9爲根結點,進行右旋,得到圖15.此時結點6,7的符號都是負的,再以結點6爲根結點左旋,最終得到最後的平衡二叉樹,如圖
平衡二叉樹實現算法
首先是需要改進二叉排序樹的結點結構,增加一個bf,用來存儲平衡因子。
typedef struct BiTNode
{
int data;
int bf;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
然後對右旋操作,代碼如下
/*對以p爲根的二叉排序樹作右旋處理,處理之後,p指向新的根結點,即處理之前的左子樹的根結點*/
void R_Rotate(BiTree *P)
{
BiTree L;
L = (*P)->lchild;
(*P)-lchild=L->rchild;
L-rchild=(*P);
(*P) = L; //P指向新的根結點
}
此函數代碼的意思就是,當傳入一個二叉排序樹P,將它的左孩子結點定義爲L,將L的右子樹變成P的左子樹,再將P改成L的右子樹,最後將L替換P成爲根結點。這樣就完成了一次右旋操作,如下圖:
左旋操作代碼哪下:
void L_Rotate(BiTree *P)
{
BiTree R;
R= (*P)->rchild;
(*P)->rchild=R->lchild; //R的左子樹掛爲P的右子樹
R->lchild=(*P);
*P=R;
}
左平衡旋轉處理的函數代碼
#define LH +1; //左高
#define EH 0; //等高
#define RH -1; //右高
/*
對於指針T所指結點爲根的二叉樹作左平衡旋轉處理
本算法結束時,指針T指向新的根結點
*/
void LeftBalance(BiTree *T)
{
BiTree L,Lr;
L=(*T)->lchild; //指向T的左子樹根結點
switch(L->bf)
{
//檢查T的碟子樹的平衡度,並作相應的平衡處理
case LH://新結點插入在T的左孩子的左子樹上,要作單右旋處理
(*T)->bf=L->bf=EH;
R_Rotate(T);
break;
case RH://新結點插入在T的左孩子的右子樹上,要作雙旋處理
Lr = L->rchild;//Lr指向T的左孩子的右子樹根
switch(Lr->bf) //修改T及其左孩子的平衡因子
{
case LH:(*T)->bf=RH;
L->bf=EH;
break;
case EH: (*T)->bf=EH;
break;
case RH:(*T)->bf=EH;
L->bf=LH;
break;
}
Lr->bf=EH;
L_Rotate(&(*T)->lchild); //對T的左子樹作左旋平衡處理
R_Rotate(T); //對T作右旋平衡處理
}
}
首先定義了三個常數變量,分別代表1、0、-1
1. 函數被調用,傳入一個需要調整平衡性的子樹T。由於LeftBalance函數被調用時,其實是已經確認當前子樹是不平衡狀態,且左子樹的高度大於右子樹的高度。換句話說,此時T的根結點應該是平衡因子BF的值大於1的數
2. 第4行,我們將T的左孩子賦值給L
3. 第5~27行是分支判斷
4. 當L的平衡因子爲LH,即爲1時,表明它與根結點的BF值符號相同,因此,第8行,將它們的BF值都改爲0,並且第9行,進行右旋操作。
5. 當L的平衡因子爲RH,即-1時,表明它與根結點的BF值符號相反,此時需要做雙旋處理。針對L的右孩子Lr的BF作判斷,修改根結點T和L的BF值。第24行當前Lr的BF改爲0
6. 對根結點的左子樹進行左旋
7. 對根結點進行右旋,完成平衡操作。
同樣的,右平衡旋轉處理的函數代碼非常類似,略()
接下來我們來看看主函數
/*
若在平衡二叉排序樹T中不存在和e相同的關鍵字,則插入一個數據元素e的新結點並返回1,否則返回0。若因插入而使二叉排序對失去平衡,則作平衡旋轉處理,布爾變量taller反應T長高與否
*/
Status InsertAVL(BiTree *T,int e,Status *taller)
{
if(!*T)
{//插入新結點,樹”長高”,置taller爲TRUE
*T=(BiTree)malloc(sizeof(BiTNode));
(*T)->data=e;
(*T)->lchild = (*T)->rchild=NULL;
(*T)->bf=EH;
*taller = TRUE;
}else{
if(e==(*T)->data){//樹中已存在e有相同關鍵字的結點則不再插入
*taller=FALSE;
return FALSE;
}
if(e<(*T)->data){
//繼續在左子樹中搜索
if(!InsertAVL(&(*T)->lchild,e,taller))
return FALSE;
if(*taller) //已插入到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{ //應繼續在右子樹中進行搜索
if(!InsertAVL(&(*T)->rchild,e,taller))
returnFALSE;
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 TRUE;
}