平衡二叉樹(AVL)圖解與實現
平衡二叉樹(Balanced BinaryTree)又被稱為AVL樹。它具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。
平衡二叉樹一般是一個有序樹,它具有二叉樹的所有性質,其遍歷操作和二叉樹的遍歷操作相同。但是由於其對二叉樹施加了額外限制,因而其新增、刪除操作都必須保證平衡二叉樹的因子被保持。
平衡二叉樹中引入了一個概念:平衡二叉樹節點的平衡因子,它指的是該節點的兩個子樹,即左子樹和右子樹的高度差,即用左子樹的高度減去右子樹的高度,如果該節點的某個子樹不存在,則該子樹的高度為0,如果高度差的絕對值超過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; } };
平衡的調整共有四種情況:分別為LL,LR,RR,RL。
下面我們通過不斷插入資料來說明幾種不同的旋轉方式:
注意:橘黃色的結點為旋轉中心,黑色結點的為離插入結點最近的失衡結點。
(1)LR型
最開始插入資料16,3,7後的結構如上圖所示,結點16失去了平衡,3為16的左孩子,7為失衡結點的左孩子的右孩子,所以為LR型,接下來通過兩次旋轉操作復衡,先通過以3為旋轉中心,進行左旋轉,結果如圖所示,然後再以7為旋轉中心進行右旋轉,旋轉後恢復平衡了。
//LR型,先左旋轉,再右旋轉 //返回:新父節點 Tree LR_rotate(Tree node) { RR_rotate(node->lchild); return LL_rotate(node); }
(2)LL型
在上面恢復平衡後我們再次插入資料11和9,發現又失去平衡了,這次失衡結點是16,11是其左孩子,9為其失衡結點的左孩子的左孩子,所以是LL型,以失衡結點的左孩子為旋轉中心進行一次右旋轉即可。
//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;
}
(3)RR型
進一步插入資料26後又再次失衡了,失衡結點為7,很明顯這是RR型,以失衡結點的右孩子為旋轉中心左旋轉一次即可。
程式碼:
//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;
}
(4)RL型
再插入18後又再次失衡了,失衡結點為16,26為其右孩子,18為其右孩子的左孩子,為RL型,以失衡結點的右孩子為旋轉中心,進行一次右旋轉,然後再次已失衡結點的右孩子為旋轉中心進行一次左旋轉變恢復了平衡。
//RL型,先右旋轉,再左旋轉
//返回:新父節點
Tree RL_rotate(Tree node)
{
LL_rotate(node->rchild);
return RR_rotate(node);
}
這就是4中旋轉方式,其實只有兩種,RR和LL,RL和LR本質上是一樣的。下面我們再次插入資料14,15,完成我們最後資料的插入操作:
又是一次LR型,按前面操作就可以了。
一、平衡二叉樹的建立
說完了平衡二叉樹的4個基本調整,基本問題就解決一大半了,接下來我們再來說說其它的操作函式,首先我們得建樹對吧,既然要建樹,那麼就得插入資料呀,來看看我們的插入函式:
//向AVL樹中插入val
//引數:根,插入資料value
//返回:新根結點
Tree Insert(Tree &root,ElementType val)
{
Tree temp=NULL;
Tree node=new AVLNode(val);
//插入結點
temp=insert_val(root,node,NULL); //呼叫真正的插入函式
if (temp)
{
update_depth(temp);
root=AVLTree(root,temp); //檢查樹是否該調整
}
else //無需插入,釋放結點
delete temp;
return root;
}
這裡我們看到,我們在插入資料裡面還有一個真正的插入函式,以及高度更新操作,還有我們AVL樹調整操作,而我們剛剛的插入函式封裝了封裝了這兩個函式,這樣方便我們的呼叫,我們可以先不管其細節如果,先理解上面的邏輯結構,我們在函式裡面建立一個結點,然後插入這個資料,當然不一定成功呢,如果這個AVL樹中有了這個資料,那麼插入就會失敗,temp將等於空,所以我們對temp可以進行判斷,再進行操作,當插入成功後,我們更新插入結點的高度資訊,其實這個結點肯定是葉子結點,不用更新也可以,因為我們在建構函式裡面已經初始化了。高度更新後,我們便開始看是否需要調整樹結構,將調整後的新根結點返回來。這就是這段程式碼的邏輯結構,很簡單吧,接下來我們在分析其分支結構。
//更新當前深度
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;
}
可以看到這個函式非常的簡單,就是直接返回當前結點的高度而已,單獨寫個函式,只是為了便於梳理邏輯結構,便於呼叫,也許大家這裡就有問題了,我們每次都是獲取當前結點裡面的高度,但是怎麼確保我們裡面每個結點的高度資訊都是準確的呢,為了我們每個結點高度資訊的準確,所以我們每插入一個結點就得一層一層的更新結點的高度資訊,因為建樹過程是從無到有,我們最開始就慢慢維護好每個結點的高度,一旦變動我們就更新一下,不管是否需要,這樣我們就可以保證準確性了,其實這個問題我們可以放在後面再來理解的。
OK,到了這裡我們還有一個個非常重要的函式沒有講解,就是AVLTree,AVL樹調整函式,裡面了前面的邏輯結構我們現在就往下看:
//AVL樹調整函式
//引數:根結點,插入結點
//返回:調整後的根結點
Tree AVLTree(Tree &root,Tree node)
{
int balance=0; //平衡因子
while (node!=NULL) //檢查其祖先是否需要調整,更新
{
update_depth(node); //更新當前結點的高度資訊
balance=is_balance(node); //獲取當前結點的平衡因子情況
if (balance>1 || balance<-1) //平衡因子超標
{
if (balance>1) //左子樹高
{
if (is_balance(node->lchild)>0) //LL型
node=LL_rotate(node);
else //LR型
node=LR_rotate(node);
}
else //右子樹高
{
if (is_balance(node->rchild)<0) //RR型
node=RR_rotate(node);
else //RL型
node=RL_rotate(node);
}
if (node->parent==NULL) //到達根結點
{
root=node; //設定新的根結點
break; //退出
}
}
node=node->parent; //依次找到其父節點
}
return root; //返回新根
}
重要看到其真面目了,很驚訝的是,怎麼這麼短呢,其實我們把各個功能分開了,這樣方便編寫,不然很混亂,這裡我們終於看到了我們的平衡因子,這裡的平衡因子我們由樹的高度得出,判斷當前這個樹是否平衡,那麼我們只要得出這個樹的左右子樹的高度變可以判斷這顆樹是否平衡了,如果不平衡,那麼我們就判斷是4中型別的那種,然後進行相應操作即可了,程式碼裡面的註釋我覺得我還是寫得很詳細了,只要認真看,結合圖解肯定是看得懂的。最後我們看到這裡有個while迴圈,其實這個迴圈就是一層一層檢查祖先是否平衡,以及更新他們的高度資訊,這樣我們就能維護好一顆樹啦,注意根結點的父節點是為空的,這樣我們最後便可以順利的退出來啦。
這裡的4種操作我在前面就給出來了,這裡就不在重複給出來了,可以看看前面,註釋很詳細的。
到這裡平衡二叉樹的建樹過程就完成了,只需平衡二叉樹的查詢函式很簡單,這裡就不在給出來,其實自己也沒有寫,當然這個可以完全自己寫,平衡二叉樹也是一顆BST樹,BST的查詢函式完全適用。
二、平衡二叉樹的刪除
前面我們說了平衡二叉樹的建樹過程,只要明白怎麼旋轉,以及怎麼進行平衡的判斷其實很簡單的,不同的判斷方法可能導致程式碼差別很大,所以選擇好的判斷方式還是很重要的。AVL樹的刪除操作其實比較簡單的,因為這裡我們以及有了調整函式,這樣我們再刪除的過程中如果一旦判斷不平衡了,那麼我們就通過平衡調整函式進行調整即可。平衡二叉樹的刪除方式和BST的刪除方式是一樣的,都是化繁為簡,如果看到這裡不知道BST怎麼刪除元素的可以參考這篇部落格:BST的建立於刪除
OK,我們這裡假設大家已經會了BST的刪除操作,我們直接進行AVL樹的操作,先看一下刪除函式:
//找到刪除的結點,執行刪除操作,並根據情況調整AVL樹
//引數:根,需要刪除的val
//返回:找到刪除結點的情況則返回新根,否則返回NULL
Tree remove(Tree &root,ElementType val)
{
static Tree *temp=NULL;
if (root==NULL)
{
temp=NULL;
return NULL;
}
else if(root->val<val) //在右子樹查詢
remove(root->rchild, val);
else if(root->val>val) //在左子樹查詢
remove(root->lchild, val);
else //找到了,標記一下
temp=&root;
if (temp)
{
if (!root->parent) //如果已經返回到最後一次(也就是root是真正的樹根)
{
Tree tmp=NULL;
tmp=remove_val(root,*temp); //執行刪除操作
return AVLTree(root,tmp); //更新AVL樹
}
return *temp;
}
return NULL;
}
OK,認真看還是很簡單的,這裡一直查詢這個需要刪除的結點,然後賦值給這個temp,這樣我們再函式即將退出的時候,進行相應的判斷操作,如果找了,那麼我們呼叫一個刪除函式,進行刪除,刪除後調整一下AVL樹,注意這裡的temp是個二級結構體指標,會引用需要刪除的結點。
//刪除操作
//引數:根,需要刪除的結點
//返回值: 返回刪除結點的父節點
Tree remove_val(Tree &root,Tree &node)
{
Tree parent=node->parent;
Tree temp=NULL;
//只有左孩子
if (node->rchild==NULL && node->lchild!=NULL)
{
temp=node;
node=node->lchild; //指向左孩子
node->parent=temp->parent;
delete temp; //釋放結點
update_depth(node); //更新當前結點資訊
}
else if(node->lchild==NULL && node->rchild!=NULL) //只有右孩子
{
temp=node;
node=node->rchild; //指向右結點
node->parent=temp->parent;
delete temp; //釋放結點
update_depth(node); //更新當前結點資訊
}
else if(node->rchild==NULL && node->lchild==NULL) //葉子結點
{
parent=node->parent; //找到其父節點
if (parent) //如果父節點存在
{
/*
if (parent->lchild==node)//當前結點是父節點的左孩子
{
parent->lchild=0; //刪掉左孩子
delete node; //釋放空間
}
else //當前結點是父節點的右孩子
{
parent->rchild=0;
delete node;
}
*/
delete node;
node=NULL;
update_depth(parent); //更新父節點高度資訊
}
else //刪除的是根
{
delete root;
root=NULL;
}
}
else //既有左孩子也有右孩子,化繁為簡
{
Tree *tmp=Find_Min(node->rchild); //找到替代元素,temp為葉子結點
node->val=(*tmp)->val; //更新值
//判斷當前葉子結點是左孩子還是右孩子。
parent=(*tmp)->parent;
/*
if (parent->lchild==temp)
{
parent->lchild=0;
delete temp;
}
else
{
parent->rchild=0;
delete temp;
}
*/
delete *tmp;
*tmp=NULL;
update_depth(parent);
}
return parent;
}
這便是我們真正的刪除函式,裡面分了幾種不同的情況,同時對於複雜的情況我們化繁為簡,這裡大家注意到,我們註釋了一段程式碼,這是在開始的時候,我沒有引用刪除結點指標,這樣需要通過父節點來進行操作,需要判斷是父節點的左孩子還是右孩子,然後再進行相應的操作,後來為了統一,把Find_Min函式改為返回其引用結構,這樣就可以不用判斷了,這樣變可以簡化很多(這和BST裡面是一樣的),不過也極容易操作出錯,而影響樹結構,可以看看幾種旋轉操作,為了方便理解,沒有改成這種結構。
OK到這裡,我們就把二叉平衡樹的刪除操作說完了,相對來說只要旋轉有了,那麼刪除也很簡單了,只是細節處理稍微麻煩點,反正一旦處理了某個結點,那麼一定要層層更新。維護好樹結構。特別是刪除操作。有可能最近祖先沒有失衡,但是稍遠的祖先確失衡了。至於刪除的圖解應該不用話出來了吧,後面我給出資料,然後自己可以在紙上畫一下,看看是否正確。
看看main函式:
int main()
{
Tree root=NULL;
root = Insert(root, 16);
root = Insert(root, 3);
//插入7後LR調整
root = Insert(root, 7);
root = Insert(root, 11);
//插入9後LL調整
root = Insert(root, 9);
//插入26後RR調整
root = Insert(root, 26);
//插入28後RL調整
root = Insert(root, 18);
root = Insert(root, 14);
//插入15後LR調整
root = Insert(root, 15);
printf("插入:\n");
printf("前序:");
PreOrder(root); // 11 7 3 9 18 15 14 16 26
printf("\n");
printf("中序:");
InOrder(root); // 3 7 9 11 14 15 16 18 26
printf("\n");
printf("刪除:\n");
//測試刪除葉子結點
// remove(root, 16);
//測試刪除只有左孩子的結點
// remove(root, 16);
// remove(root, 15);
//測試刪除只有右孩子的結點
// remove(root, 14);
// remove(root, 15);
//測試刪除有左右孩子的結點
// remove(root, 18);
//刪除26後進行LR型調整
remove(root, 26);
//刪除18後進行RR型
remove(root, 18);
remove(root, 3);
remove(root, 9);
//刪除7過進行RL調整
remove(root, 7);
//刪除11後進行LL調整
remove(root, 11);
//把結點刪除完
// remove(root, 15);
// remove(root, 14);
// remove(root, 16);
printf("前序:");
PreOrder(root);
printf("\n");
printf("中序:");
InOrder(root);
printf("\n");
return 0;
}
前面的插入就不說了,只要說說刪除,因為測試的時候,資料很重要,開始就是測試的不夠好,導致錯誤程式碼混過去了,後來改正了,不過可能還是有bug,忘各位不吝指出。
//測試刪除葉子結點
// remove(root, 16);
//測試刪除只有左孩子的結點
// remove(root, 16);
// remove(root, 15);
//測試刪除只有右孩子的結點
// remove(root, 14);
// remove(root, 15);
//測試刪除有左右孩子的結點
// remove(root, 18);
這幾個測試點,是單獨測試的,也就是它們之間是獨立的,分別測試4種不同的情況,比如測試第二種的是否,請註釋第一組和其它組,不過亂測試還是可以的,後面的旋轉測試,是一步接著一步的,不是獨立的。
OK,終於說完了,下面貼出全部程式碼,希望和大家一起學習,如果有問題,希望各位可以指出,謝謝啦。
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
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;
}
};
Tree insert_val(Tree&,Tree,Tree);
Tree remove(Tree&,ElementType);
Tree remove_val(Tree &,Tree &);
void update_depth(Tree);
Tree AVLTree(Tree&,Tree);
Tree LL_rotate(Tree);
Tree RL_rotate(Tree);
Tree RR_rotate(Tree);
Tree LR_rotate(Tree);
int get_balance(Tree);
int is_balance(Tree);
Tree *Find_Min(Tree&);
//向AVL樹中插入val
//引數:根,插入資料value
//返回:新根結點
Tree Insert(Tree &root,ElementType val)
{
Tree temp=NULL;
Tree node=new AVLNode(val);
//插入結點
temp=insert_val(root,node,NULL); //呼叫真正的插入函式
if (temp)
{
update_depth(temp);
root=AVLTree(root,temp); //檢查樹是否該調整
}
else //無需插入,釋放結點
delete temp;
return root;
}
//插入函式
//引數:根節點,待插結點,待插結點的父節點
//返回:插入結點
Tree insert_val(Tree &root,Tree node,Tree parent)
{
if (root==NULL)
{
root=node;
node->parent=parent; //設定當前結點的父結點
return root; //返回插入結點
}
if (node->val<root->val) //插左子樹
return insert_val(root->lchild, node,root);
else if(node->val>root->val) //插右子樹
return insert_val(root->rchild, node,root);
else //已存在該結點,停止插入操作,返回NULL
return NULL;
}
//AVL樹調整函式
//引數:根結點,插入結點
//返回:調整後的根結點
Tree AVLTree(Tree &root,Tree node)
{
int balance=0; //平衡因子
while (node!=NULL) //檢查其祖先是否需要調整,更新
{
update_depth(node); //更新當前結點的高度資訊
balance=is_balance(node); //獲取當前結點的平衡因子情況
if (balance>1 || balance<-1) //平衡因子超標
{
if (balance>1) //左子樹高
{
if (is_balance(node->lchild)>0) //LL型
node=LL_rotate(node);
else //LR型
node=LR_rotate(node);
}
else //右子樹高
{
if (is_balance(node->rchild)<0) //RR型
node=RR_rotate(node);
else //RL型
node=RL_rotate(node);
}
if (node->parent==NULL) //到達根結點
{
root=node; //設定新的根結點
break; //退出
}
}
node=node->parent; //依次找到其父節點
}
return root; //返回新根
}
//查詢最小結點
Tree *Find_Min(Tree &root)
{
if (root->lchild)
{
return Find_Min(root->lchild);
}
return &root;
}
//刪除操作
//引數:根,需要刪除的結點
//返回值: 返回刪除結點的父節點
Tree remove_val(Tree &root,Tree &node)
{
Tree parent=node->parent;
Tree temp=NULL;
//只有左孩子
if (node->rchild==NULL && node->lchild!=NULL)
{
temp=node;
node=node->lchild; //指向左孩子
node->parent=temp->parent;
delete temp; //釋放結點
update_depth(node); //更新當前結點資訊
}
else if(node->lchild==NULL && node->rchild!=NULL) //只有右孩子
{
temp=node;
node=node->rchild; //指向右結點
node->parent=temp->parent;
delete temp; //釋放結點
update_depth(node); //更新當前結點資訊
}
else if(node->rchild==NULL && node->lchild==NULL) //葉子結點
{
parent=node->parent; //找到其父節點
if (parent) //如果父節點存在
{
/*
if (parent->lchild==node)//當前結點是父節點的左孩子
{
parent->lchild=0; //刪掉左孩子
delete node; //釋放空間
}
else //當前結點是父節點的右孩子
{
parent->rchild=0;
delete node;
}
*/
delete node;
node=NULL;
update_depth(parent); //更新父節點高度資訊
}
else //刪除的是根
{
delete root;
root=NULL;
}
}
else //既有左孩子也有右孩子,化繁為簡
{
Tree *tmp=Find_Min(node->rchild); //找到替代元素,temp為葉子結點
node->val=(*tmp)->val; //更新值
//判斷當前葉子結點是左孩子還是右孩子。
parent=(*tmp)->parent;
/*
if (parent->lchild==temp)
{
parent->lchild=0;
delete temp;
}
else
{
parent->rchild=0;
delete temp;
}
*/
delete *tmp;
*tmp=NULL;
update_depth(parent);
}
return parent;
}
//找到刪除的結點,執行刪除操作,並根據情況調整AVL樹
//引數:根,需要刪除的val
//返回:找到刪除結點的情況則返回新根,否則返回NULL
Tree remove(Tree &root,ElementType val)
{
static Tree *temp=NULL;
if (root==NULL)
{
temp=NULL;
return NULL;
}
else if(root->val<val) //在右子樹查詢
remove(root->rchild, val);
else if(root->val>val) //在左子樹查詢
remove(root->lchild, val);
else //找到了,標記一下
temp=&root;
if (temp)
{
if (!root->parent) //如果已經返回到最後一次(也就是root是真正的樹根)
{
Tree tmp=NULL;
tmp=remove_val(root,*temp); //執行刪除操作
return AVLTree(root,tmp); //更新AVL樹
}
return *temp;
}
return NULL;
}
//獲取當前結點的深度
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);
}
//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;
}
//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;
}
//LR型,先左旋轉,再右旋轉
//返回:新父節點
Tree LR_rotate(Tree node)
{
RR_rotate(node->lchild);
return LL_rotate(node);
}
//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;
}
}
//前序
void PreOrder(Tree root)
{
if (root==NULL)
return;
printf("%d ",root->val);
PreOrder(root->lchild);
PreOrder(root->rchild);
}
//中序
void InOrder(Tree root)
{
if (root==NULL)
return;
InOrder(root->lchild);
printf("%d ",root->val);
InOrder(root->rchild);
}
int main()
{
Tree root=NULL;
root = Insert(root, 16);
root = Insert(root, 3);
//插入7後LR調整
root = Insert(root, 7);
root = Insert(root, 11);
//插入9後LL調整
root = Insert(root, 9);
//插入26後RR調整
root = Insert(root, 26);
//插入28後RL調整
root = Insert(root, 18);
root = Insert(root, 14);
//插入15後LR調整
root = Insert(root, 15);
printf("插入:\n");
printf("前序:");
PreOrder(root); // 11 7 3 9 18 15 14 16 26
printf("\n");
printf("中序:");
InOrder(root); // 3 7 9 11 14 15 16 18 26
printf("\n");
printf("刪除:\n");
//測試刪除葉子結點
// remove(root, 16);
//測試刪除只有左孩子的結點
// remove(root, 16);
// remove(root, 15);
//測試刪除只有右孩子的結點
// remove(root, 14);
// remove(root, 15);
//測試刪除有左右孩子的結點
// remove(root, 18);
//刪除26後進行LR型調整
remove(root, 26);
//刪除18後進行RR型
remove(root, 18);
remove(root, 3);
remove(root, 9);
//刪除7過進行RL調整
remove(root, 7);
//刪除11後進行LL調整
remove(root, 11);
//把結點刪除完
// remove(root, 15);
// remove(root, 14);
// remove(root, 16);
printf("前序:");
PreOrder(root);
printf("\n");
printf("中序:");
InOrder(root);
printf("\n");
return 0;
}
程式我反覆測試過,目前還沒有發現問題,當然也是還存在bug,不管是程式碼還是思考過程,如果有問題,還希望各位不吝指教。感謝。
main函式太長,最後執行結果:
三、平衡樹的3+4重構
平衡二叉樹也是一顆BST樹,那麼BST的特點,平衡二叉樹也應該具有,那麼BST樹又有什麼特點呢,特點是有序,什麼有序呢,中序遍歷序列有序,也就是說BST樹的中序序列是單調的,這個很容易就證明了,對根節點而已,其左子樹的肯定比它本身要小,其右子樹的值肯定比其值要大,而中序序列是左根右,所以BST樹的中序序列肯定是單調的。這個是非常重要的性質,稍後我們將用到它。
我們設g(x)為最低的失衡結點,考察祖孫三代:g~p~v,按照中序遍歷次序將其重新命名為:a<b<c。
它們總共擁有互不相交的四顆(可能為空的)子樹,按照中序遍歷次序,將其重新命名為:T0<T1<T2<T3。
此時,如果我們依然按照中序的遍歷次序將這兩個序列混合起來,就可以得到一個長度為7的序列。在這個序列中三個結點a,b,c,必然是鑲嵌於這4棵子樹之間
實際上無論是哪種具體的情況,經過這樣的重新命名之後,按照中序遍歷的次序,必然是從T0到a,再從a到T1,再從T1到b,然後從b到T2,再從T2到c,最終由c到T3,這就是BST單調性的體現
因此我們可以統一的將這三個頂點abc以及這4棵子樹,按照下面的拓撲關係直接的拼接起來,這樣的一種拼接是針對於三個結點,以及下屬的4棵子樹而言的,所以也稱作3+4重構
無論是插入還是刪除,無論是單旋還是雙旋,最終的效果都應該是這樣一種形式。
下面我們給出我們的3+4重構函式的程式碼:
//3+4重構函式
//引數:見分析
//返回:新根
Tree connect34(Tree &a,Tree &b,Tree &c,Tree &T0,Tree &T1,Tree &T2,Tree &T3)
{
a->lchild=T0;
if (T0)
T0->parent=a;
a->rchild=T1;
if(T1)
T1->parent=a;
update_depth(a);
c->lchild=T2;
if(T2)
T2->parent=c;
c->rchild=T3;
if(T3)
T3->parent=c;
update_depth(c);
b->lchild=a;
a->parent=b;
b->rchild=c;
c->parent=b;
update_depth(b);
return b;
}
可以看見很簡單,從此以後我們就不用各種旋轉了,因此對於調整AVL樹結構的函式,也需要改,不過我們就再原來的基礎上調整一下就就可以了。
Tree rotateAt(Tree &root,Tree &node)
{
Tree son,temp;
Tree grandson;
int balance=0; //平衡因子
while (node!=NULL) //檢查其祖先是否需要調整,更新
{
update_depth(node); //更新當前結點的高度資訊
balance=is_balance(node); //獲取當前結點的平衡因子情況
if (balance>1 || balance<-1) //平衡因子超標
{
if (balance>1) //左子樹高
{
if (is_balance(node->lchild)>0) //LL型
{
//找祖孫三代,後面的類似
son=node->lchild; //找其左孩子
grandson=son->lchild; //找其左孩子的左孩子
son->parent=node->parent; //設定更新後的son的父節點
temp=node;
//重構
node=connect34(grandson, son, node, grandson->lchild, grandson->rchild, son->rchild, node->rchild);
setchild(son, temp, node);//設定son父節點的孩子為node
}
else //LR型
{
son=node->lchild;
grandson=son->rchild;
grandson->parent=node->parent;
temp=node;
node=connect34(son, grandson, node, son->lchild, grandson->lchild, grandson->rchild, node->rchild);
setchild(grandson, temp, node); //設定grandson父節點的孩子為node
}
}
else //右子樹高
{
if (is_balance(node->rchild)<0) //RR型
{
son=node->rchild;
grandson=son->rchild;
son->parent=node->parent;
temp=node;
node=connect34(node, son, grandson, node->lchild, son->lchild, grandson->lchild, grandson->rchild);
setchild(son, temp, node); //設定son父節點的孩子為node
}
else //RL型
{
son=node->rchild;
grandson=son->lchild;
grandson->parent=node->parent;
temp=node;
node=connect34(node, grandson, son, node->lchild, grandson->lchild, grandson->rchild, son->rchild);
setchild(grandson, temp, node); //設定grandson父節點的孩子為node
}
}
if (node->parent==NULL) //到達根結點
{
root=node; //設定新的根結點
break; //退出
}
}
node=node->parent; //依次找到其父節點
}
return root; //返回新根
}
額,看著好複雜的樣子,其實仔細觀察,不難看出,我們只是在原來的基礎上刪除了旋轉操作,然後把3+4重構函式加了進來,在進行重構之前,首先需要找到abc,以及T0,T1,T2,T3,這個看一下程式就明白了,不夠值得注意的是,要重新設定它們的連線關係,尤其是abc,後它們祖先的關係,這裡還有一個setchild函式,就是用來連線abc和它們祖先的函式,abc的拓撲在變,也就是最後son可能就成parent了,所以需要更新son的父節點。
void setchild(Tree &g,Tree &temp,Tree &node)
{
if (g->parent)
{
if (g->parent->lchild==temp)
g->parent->lchild=node;
else
g->parent->rchild=node;
}
}
OK,這就是setchild函式,到這裡平衡二叉樹的3+4重構就講完了,簡單吧,下面給出其總程式碼,測試方式同上面的旋轉的平衡二叉樹。
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
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;
}
};
Tree insert_val(Tree&,Tree,Tree);
Tree remove(Tree&,ElementType);
Tree remove_val(Tree &,Tree &);
void update_depth(Tree);
int get_balance(Tree);
int is_balance(Tree);
Tree *Find_Min(Tree&);
Tree connect34(Tree&,Tree&,Tree&,Tree&,Tree&,Tree&,Tree&);
Tree rotateAt(Tree&,Tree&);
void setchild(Tree &,Tree &,Tree &);
//向AVL樹中插入val
//引數:根,插入資料value
//返回:新根結點
Tree Insert(Tree &root,ElementType val)
{
Tree temp=NULL;
Tree node=new AVLNode(val);
//插入結點
temp=insert_val(root,node,NULL); //呼叫真正的插入函式
if (temp)
{
update_depth(temp);
root=rotateAt(root, temp);//檢查樹是否該調整
}
else //無需插入,釋放結點
delete temp;
return root;
}
//插入函式
//引數:根節點,待插結點,待插結點的父節點
//返回:插入結點
Tree insert_val(Tree &root,Tree node,Tree parent)
{
if (root==NULL)
{
root=node;
node->parent=parent; //設定當前結點的父結點
return root; //返回插入結點
}
if (node->val<root->val) //插左子樹
return insert_val(root->lchild, node,root);
else if(node->val>root->val) //插右子樹
return insert_val(root->rchild, node,root);
else //已存在該結點,停止插入操作,返回NULL
return NULL;
}
//3+4重構函式
//引數:見分析
//返回:新根
Tree connect34(Tree &a,Tree &b,Tree &c,Tree &T0,Tree &T1,Tree &T2,Tree &T3)
{
a->lchild=T0;
if (T0)
T0->parent=a;
a->rchild=T1;
if(T1)
T1->parent=a;
update_depth(a);
c->lchild=T2;
if(T2)
T2->parent=c;
c->rchild=T3;
if(T3)
T3->parent=c;
update_depth(c);
b->lchild=a;
a->parent=b;
b->rchild=c;
c->parent=b;
update_depth(b);
return b;
}
Tree rotateAt(Tree &root,Tree &node)
{
Tree son,temp;
Tree grandson;
int balance=0; //平衡因子
while (node!=NULL) //檢查其祖先是否需要調整,更新
{
update_depth(node); //更新當前結點的高度資訊
balance=is_balance(node); //獲取當前結點的平衡因子情況
if (balance>1 || balance<-1) //平衡因子超標
{
if (balance>1) //左子樹高
{
if (is_balance(node->lchild)>0) //LL型
{
//找祖孫三代,後面的類似
son=node->lchild; //找其左孩子
grandson=son->lchild; //找其左孩子的左孩子
son->parent=node->parent; //設定更新後的son的父節點
temp=node;
//重構
node=connect34(grandson, son, node, grandson->lchild, grandson->rchild, son->rchild, node->rchild);
setchild(son, temp, node);//設定son父節點的孩子為node
}
else //LR型
{
son=node->lchild;
grandson=son->rchild;
grandson->parent=node->parent;
temp=node;
node=connect34(son, grandson, node, son->lchild, grandson->lchild, grandson->rchild, node->rchild);
setchild(grandson, temp, node); //設定grandson父節點的孩子為node
}
}
else //右子樹高
{
if (is_balance(node->rchild)<0) //RR型
{
son=node->rchild;
grandson=son->rchild;
son->parent=node->parent;
temp=node;
node=connect34(node, son, grandson, node->lchild, son->lchild, grandson->lchild, grandson->rchild);
setchild(son, temp, node); //設定son父節點的孩子為node
}
else //RL型
{
son=node->rchild;
grandson=son->lchild;
grandson->parent=node->parent;
temp=node;
node=connect34(node, grandson, son, node->lchild, grandson->lchild, grandson->rchild, son->rchild);
setchild(grandson, temp, node); //設定grandson父節點的孩子為node
}
}
if (node->parent==NULL) //到達根結點
{
root=node; //設定新的根結點
break; //退出
}
}
node=node->parent; //依次找到其父節點
}
return root; //返回新根
}
void setchild(Tree &g,Tree &temp,Tree &node)
{
if (g->parent)
{
if (g->parent->lchild==temp)
g->parent->lchild=node;
else
g->parent->rchild=node;
}
}
//查詢最小結點
Tree *Find_Min(Tree &root)
{
if (root->lchild)
{
return Find_Min(root->lchild);
}
return &root;
}
//刪除操作
//引數:根,需要刪除的結點
//返回值: 返回刪除結點的父節點
Tree remove_val(Tree &root,Tree &node)
{
Tree parent=node->parent;
Tree temp=NULL;
//只有左孩子
if (node->rchild==NULL && node->lchild!=NULL)
{
temp=node;
node=node->lchild; //指向左孩子
node->parent=temp->parent;
delete temp; //釋放結點
update_depth(node); //更新當前結點資訊
}
else if(node->lchild==NULL && node->rchild!=NULL) //只有右孩子
{
temp=node;
node=node->rchild; //指向右結點
node->parent=temp->parent;
delete temp; //釋放結點
update_depth(node); //更新當前結點資訊
}
else if(node->rchild==NULL && node->lchild==NULL) //葉子結點
{
parent=node->parent; //找到其父節點
if (parent) //如果父節點存在
{
delete node;
node=NULL;
update_depth(parent); //更新父節點高度資訊
}
else //刪除的是根
{
delete root;
root=NULL;
}
}
else //既有左孩子也有右孩子,化繁為簡
{
Tree *tmp=Find_Min(node->rchild); //找到替代元素,temp為葉子結點
node->val=(*tmp)->val; //更新值
//判斷當前葉子結點是左孩子還是右孩子。
parent=(*tmp)->parent;
delete *tmp;
*tmp=NULL;
update_depth(parent);
}
return parent;
}
//找到刪除的結點,執行刪除操作,並根據情況調整AVL樹
//引數:根,需要刪除的val
//返回:找到刪除結點的情況則返回新根,否則返回NULL
Tree remove(Tree &root,ElementType val)
{
static Tree *temp=NULL;
if (root==NULL)
{
temp=NULL;
return NULL;
}
else if(root->val<val) //在右子樹查詢
remove(root->rchild, val);
else if(root->val>val) //在左子樹查詢
remove(root->lchild, val);
else //找到了,標記一下
temp=&root;
if (temp)
{
if (!root->parent) //如果已經返回到最後一次(也就是root是真正的樹根)
{
Tree tmp=NULL;
tmp=remove_val(root,*temp); //執行刪除操作
return rotateAt(root, tmp);
}
return *temp;
}
return NULL;
}
//獲取當前結點的深度
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);
}
//更新當前深度
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;
}
}
//前序
void PreOrder(Tree root)
{
if (root==NULL)
return;
printf("%d ",root->val);
PreOrder(root->lchild);
PreOrder(root->rchild);
}
//中序
void InOrder(Tree root)
{
if (root==NULL)
return;
InOrder(root->lchild);
printf("%d ",root->val);
InOrder(root->rchild);
}
int main()
{
Tree root=NULL;
root = Insert(root, 16);
root = Insert(root, 3);
//插入7後LR調整
root = Insert(root, 7);
root = Insert(root, 11);
//插入9後LL調整
root = Insert(root, 9);
//插入26後RR調整
root = Insert(root, 26);
//插入18後RL調整
root = Insert(root, 18);
root = Insert(root, 14);
//插入15後LR調整
root = Insert(root, 15);
printf("插入:\n");
printf("前序:");
PreOrder(root); // 11 7 3 9 18 15 14 16 26
printf("\n");
printf("中序:");
InOrder(root); // 3 7 9 11 14 15 16 18 26
printf("\n");
printf("刪除:\n");
//測試刪除葉子結點
// remove(root, 16);
//測試刪除只有左孩子的結點
// remove(root, 16);
// remove(root, 15);
//測試刪除只有右孩子的結點
// remove(root, 14);
// remove(root, 15);
//測試刪除有左右孩子的結點
// remove(root, 18);
//刪除26後進行LR型調整
remove(root, 26);
//刪除18後進行RR型
remove(root, 18);
remove(root, 3);
remove(root, 9);
//刪除7過進行RL調整
remove(root, 7);
//刪除11後進行LL調整
remove(root, 11);
//把結點刪除完
// remove(root, 15);
// remove(root, 14);
// remove(root, 16);
printf("前序:");
PreOrder(root);
printf("\n");
printf("中序:");
InOrder(root);
printf("\n");
return 0;
}
程式我反覆測試過,目前還沒有發現問題,當然也是還存在bug,不管是程式碼還是思考過程,如果有問題,還希望各位不吝指教。感謝。
注:程式中大量使用了引用和指標,如果對指標有疑惑的可以參考C語言指標初探