動畫演示平衡二叉樹旋轉
我們知道在二叉查詢樹中,如果插入元素的順序接近有序,那麼二叉查詢樹將退化為連結串列,從而導致二叉查詢樹的查詢效率大為降低。如何使得二叉查詢樹無論在什麼樣情況下都能使它的形態最大限度地接近滿二叉樹以保證它的查詢效率呢?
前蘇聯科學家G.M. Adelson-Velskii 和 E.M. Landis給出了答案。他們在1962年發表的一篇名為 《An algorithm for the organization of information》的文章中提出了一種自平衡二叉查詢樹(self-balancing binary search tree)。這種二叉查詢樹在插入和刪除操作中,可以通過一系列的旋轉操作來保持平衡,從而保證了二叉查詢樹的查詢效率。最終這種二叉查詢樹以他們的名字命名為“AVL-Tree”,它也被稱為平衡二叉樹(Balanced Binary Tree)。這裡所說的平衡使我們想到了中庸之道,但有句話說得好,“中不偏,庸不易”。學會這種平衡術是一個相當痛苦的過程。
什麼是平衡
為了保證平衡,AVL樹中的每個結點都有一個平衡因子(balance factor,以下用BF表示),它表示這個結點的左、右子樹的高度差,也就是左子樹的高度減去右子樹的高度的結果值。AVL樹上所有結點的BF值只能是-1、0、1。反之,只要二叉樹上一個結點的BF的絕對值大於1,則該二叉樹就不是平衡二叉樹。圖1演示了平衡二叉樹和非平衡二叉樹。
AVL樹的構造
如何構造一棵平衡二叉樹呢?動態地調整二叉查詢樹平衡的方法為:每插入一個結點後,首先檢查是否破壞了樹的平衡性,如果因插入結點而破壞了二叉查詢樹的平衡,則找出離插入點最近的不平衡結點,然後將該不平衡結點為根的子樹進行旋轉操作,我們稱該不平衡結點為旋轉根,以該旋轉根為根的子樹稱為最小不平衡子樹。失衡狀態可歸納為4種,它們對應著4種旋轉型別。下面使用了Flash動畫演示了這四種旋轉型別,請確保你的電腦安裝了Flash8.0以上版本的播放器,並且瀏覽器允許使用Flash。做這幾個動畫純屬好玩,希望有一天可以使用Silverlight做這些的動畫。不過好象現在還沒什麼部落格支援。
l LL型旋轉
如以上動畫所示,插入結點5後,結點50的BF值由1變為2,此時結點50為旋轉根。這種插入結點50的左孩子的左子樹而導致失衡的情況需要進行LL旋轉(LL意為左左)。可以觀察到,雖然結點50的BF值由1變為了0,但最小不平衡子樹在插入結點5前和旋轉後的高度不變。
l RR型旋轉
如以上動畫所示,插入結點90後,結點25的BF值由-1變為-2,此時結點25為旋轉根。這種插入結點25的右孩子的右子樹而導致失衡的情況需要進行RR旋轉。最小不平衡子樹在插入結點90前和旋轉後的高度不變。
l LR型旋轉
插入旋轉根的左孩子的右子樹而導致失衡的情況需要進行LR旋轉。這裡演示了LR(L)和LR(R) 兩種情況。插入結點前和旋轉後的最小不平衡子樹高度不變。
l RL型旋轉
插入旋轉根的右孩子的左子樹而導致失衡的情況需要進行RL旋轉。這裡演示了RL(L)和RL(R) 兩種情況。插入結點前和旋轉後的最小不平衡子樹高度不變。
以上動畫只演示了幾種旋轉型別的較複雜的情況,並沒有全部演示,比如旋轉根的左子樹或右子樹為空的情況,具體演算法請參見稍後的程式碼。
AVL樹上結點的插入
AVL演算法的思想理解起來還是不太困難的,但如果真要使用程式碼實現就沒那麼簡單了,它擁有超高的演算法實現複雜度。我查了很多資料,大部分只給出主要演算法程式碼,對於如何回溯修改BF值,如何處理不需要旋轉的情況絕口不提,甚至對刪除演算法直接忽略。上網找資料,中文的,英文的全找了,大部分寫程式碼不加註釋,狂汗....,實在看不下去。大部分程式碼使用遞迴演算法,C#實現更是少得可憐,在國外網站找到一個,但使用了三叉連結串列實現,多加了一個parent指標,總之無法找到讓人滿意的程式碼。最後一咬牙一跺腳,自己實現。最讓人頭痛的莫過於如何處理插入和刪除後的回溯和修改BF值,慶幸的是最終還是按照我最初的想法比較漂亮地實現了AVL樹。優點是:無遞迴;無parent指標;插入和刪除操作使用同一旋轉方法,使程式碼更為簡化。缺點是:為了兼顧效率,有些地方的處理比較特殊,程式碼很難完全讀懂。
下面對本演算法做原理上的介紹:
1、 如何回溯修改祖先結點的平衡因子
我們知道,在AVL樹上插入一個新結點後,有可能導致其他結點BF值的改變,哪些結點的BF值會被改變?如何計算新的BF值呢?要解決這些問題,我們必須理解以下幾個要點:
l 只有根結點到插入結(橙色結點)點路徑(稱為插入路徑)上的結點的BF值會被改變。如圖2所示,只有插入路徑上結點(灰色結點)的BF值被改變,其他非插入路徑上結點的BF值不變。
l 當一個結點插入到某個結點的左子樹時,該結點的BF值加1(如圖2的結點50、43);當一個結點插入到某個結點的右子樹時,該結點的BF值減1(如圖2的結點25、30)。如何在程式中判斷一個結點是插入到左子樹還是右子樹呢?很簡單,根據二叉查詢樹的特性可以得出結論:如果插入結點小於某個結點,則必定是插入到這個結點的左子樹中;如果如果插入結點大於某個結點,則必定插入到這個結點的右子樹中。
l 修改BF值的操作需從插入點開始向上回溯至根結點依次進行,當路徑上某個結點BF值修改後變為0,則修改停止。如圖3所示,插入結點30後,首先由於30<43,將結點43的BF值加1,使得結點43的BF值由0變為 1;接下來由於30>25,結點25的BF值由1改為0;此時結點25的BF值為0,停止回溯,不需要再修改插入路徑上結點50的平衡因子。道理很簡單:當結點的BF值由1或-1變為0,表明高度小的子樹添加了新結點,樹的高度沒有增加,所以不必修改祖先結點的平衡因子;當結點的BF值由0變為1或-1時,表明原本等高左右子樹由於一邊變高而導致失衡,整棵子樹的高度變高,所以必須向上修改祖先結點的BF值。
1、 何時進行旋轉操作?如何判斷作什麼型別的旋轉?
在回溯修改祖先結點的平衡因子時,如果碰到某個結點的平衡因子變為2或-2,表明AVL樹失衡,這時需要以該結點為旋轉根,對最小不平衡子樹進行旋轉操作。由於是從插入點開始回溯,所以最先碰到的BF值變為2或-2的結點必定為最小不平衡子樹的根結點。如圖4所示,插入39後,43和50兩個結點的BF值都會變為2,而必定先訪問到結點43,所以43是最小不平衡子樹的根。根據以上Flash動畫演示所示,旋轉操作完成後,最小不平衡子樹插入結點前和旋轉完成後的高度不變,所以可以得出結論:旋轉操作完成後,無需再回溯修改祖先的BF值。這樣,圖4中的結點25和50的平衡因子實際上在插入結點操作完成後的BF值不變(對比圖2)。
可以通過旋轉根及其孩子的BF值來決定作什麼型別的旋轉操作:
l 當旋轉根的BF值為2時:
如果旋轉根的左孩子的BF值為1,則進行LL型旋轉;
如果旋轉根的左孩子的BF值為-1,則進行LR型旋轉。
l 當旋轉根的BF值為-2時:
如果旋轉根的右孩子的BF值為1,則進行RL型旋轉;
如果旋轉根的右孩子的BF值為-1,則進行RR型旋轉。
可通過觀察之前的Flash動畫檢驗以上結論。
1、 如何儲存插入路徑?
可以使用棧來儲存插入路徑上的各個結點,但由於棧是由陣列抽象而來,為了進一步加快AVL樹的執行速度,我直接使用陣列存放插入路徑,這樣可以減少方法的呼叫,儘量避免一些不必要的操作。
如果實現AVL樹實現索引器,而在索引器中使用int32,那麼AVL樹元素的長度不會超過一個32位整數的最大值。一個深度為32的滿二叉樹可以存放結點數為:2^32-1=4294967295,這個值已經遠遠超出32位的整數範圍,所以我將陣列的長度定為32。這樣就不必如ArrayList那樣進行擴容操作了。另外本程式還使用了一個成員變數p用於指示當前訪問結點,由於p指標的存在可以不必在每次進行插入和刪除操作後清空陣列中的元素,進一步增加了AVL樹的執行速度。
使用陣列的另一個好處是可以隨時訪問旋轉根的雙親結點,以方便進行旋轉操作時修改根結點。
AVL樹上結點的刪除
AVL樹的刪除操作與插入操作有許多相似之處,它的大體步驟如下:
⑴用二叉查詢樹的刪除演算法找到並刪除結點(這裡簡稱為刪除點);
⑵沿刪除點向上回溯,必要時,修改祖先結點的BF值;
⑶回溯途中,一旦發現某個祖先的BF值失衡,如插入操作那樣旋轉不平衡子樹使之變為平衡,跟插入操作不同的是,旋轉完成後,回溯不能停止,也就是說在AVL樹上刪除一個結點有可能引起多次旋轉。
AVL樹上的刪除和插入操作雖然大體相似,但還是有一些不同之處,大家需要注意以下幾點:
1、 回溯方式的不同
在刪除結點的回溯過程中,當某個結點的BF值變為1或-1時,則停止回溯。這一點同插入操作正好相反,因為BF值由0變為1或-1,表明原本平衡的子樹由於某個結點的刪除導致了不平衡,子樹的總體高度不變,所以不再需要向上回溯。
2、 旋轉方式的不同
如圖5所示:刪除AVL樹中的結點25導致結點50的BF值由原來的-1變為-2,但旋轉根50的右孩子的BF值為0,這種情況在前面所講的旋轉操作中並不存在,那麼是需要對它進行RR旋轉還是RL旋轉呢?正確方法是使用RR旋轉,所不同之處是旋轉後的BF值不同,需要單獨處理。需要注意,這種情況在插入操作時不可能發生,LL旋轉也存在型別的情況。另外旋轉完成後樹的整體高度沒有改變,所以大部分情況下旋轉操作完成後,子樹的高度降低,需要繼續向上回溯修改祖先的BF值,而只有這種情況由於子樹的高度未改變,所以停止回溯。
3、 刪除點的選擇特例
在二叉查詢樹中,我們知道當刪除點p既有左子樹,又有右子樹,此時可以令p的中序遍歷直接前驅結點代替p,然後再從二叉查詢樹中刪除它的直接前驅。如圖7.13所示,結點5既有左子樹,又有右子樹,它的直接前驅結點為4。在刪除結點5時,首先用結點4代替結點5,然後再刪除結點4完成刪除操作。這裡需要注意的是此時必須將刪除前的結點4作為刪除點來進行向上回溯操作,而不是結點5。
AVL樹的程式碼實現
這裡沒有給出AVL樹的泛型實現,它只存放整數。因為如果使用泛型實現並按照微軟慣例,使用鍵/值對實現,那麼程式碼真的就很難讀懂了。以這個程式碼為基礎,改為泛型實現是很容易的事。另外C#中沒AVL樹的實現,而實現了紅黑樹,說明紅黑樹更有效率,所以也不必將AVL泛型化,程式碼忽略了部分出錯可能。紅黑樹將在後面講解。
public class BinarySearchTree : IBinaryTree //實現畫樹介面
{ //成員變數
private Node _head; //頭指標
private Node[] path = new Node[32]; //記錄訪問路徑上的結點
private int p; //表示當前訪問到的結點在_path上的索引
INode IBinaryTree.Head //顯式介面實現
{
get { return (INode)_head; }
}
public bool Add(int value) //新增一個元素
{ //如果是空樹,則新結點成為二叉排序樹的根
if (_head == null)
{
_head = new Node(value);
_head.BF = 0;
return true;
}
p = 0;
//prev為上一次訪問的結點,current為當前訪問結點
Node prev = null, current = _head;
while (current != null)
{
path[p++] = current; //將路徑上的結點插入陣列
//如果插入值已存在,則插入失敗
if (current.Data == value)
{
return false;
}
prev = current;
//當插入值小於當前結點,則繼續訪問左子樹,否則訪問右子樹
current = (value < prev.Data) ? prev.Left : prev.Right;
}
current = new Node(value); //建立新結點
current.BF = 0;
if (value < prev.Data) //如果插入值小於雙親結點的值
{
prev.Left = current; //成為左孩子
}
else //如果插入值大於雙親結點的值
{
prev.Right = current; //成為右孩子
}
path[p] = current; //將新元素插入陣列path的最後
//修改插入點至根結點路徑上各結點的平衡因子
int bf = 0;
while (p > 0)
{ //bf表示平衡因子的改變數,當新結點插入左子樹,則平衡因子+1
//當新結點插入右子樹,則平衡因子-1
bf = (value < path[p - 1].Data) ? 1 : -1;
path[--p].BF += bf; //改變當父結點的平衡因子
bf = path[p].BF; //獲取當前結點的平衡因子
//判斷當前結點平衡因子,如果為0表示該子樹已平衡,不需再回溯
//而改變祖先結點平衡因子,此時新增成功,直接返回
if (bf == 0)
{
return true;
}
else if (bf == 2 || bf == -2) //需要旋轉的情況
{
RotateSubTree(bf);
return true;
}
}
return true;
}
//刪除指定值
public bool Remove(int value)
{
p = -1;
//parent表示雙親結點,node表示當前結點
Node node = _head;
//尋找指定值所在的結點
while (node != null)
{
path[++p] = node;
//如果找到,則呼叫RemoveNode方法刪除結點
if (value == node.Data)
{
RemoveNode(node);//現在p指向被刪除結點
return true; //返回true表示刪除成功
}
if (value < node.Data)
{ //如果刪除值小於當前結點,則向左子樹繼續尋找
node = node.Left;
}
else
{ //如果刪除值大於當前結點,則向右子樹繼續尋找
node = node.Right;
}
}
return false; //返回false表示刪除失敗
}
//刪除指定結點
private void RemoveNode(Node node)
{
Node tmp = null;
//當被刪除結點存在左右子樹時
if (node.Left != null && node.Right != null)
{
tmp = node.Left; //獲取左子樹
path[++p] = tmp;
while (tmp.Right != null) //獲取node的中序遍歷前驅結點,並存放於tmp中
{ //找到左子樹中的最右下結點
tmp = tmp.Right;
path[++p] = tmp;
}
//用中序遍歷前驅結點的值代替被刪除結點的值
node.Data = tmp.Data;
if (path[p - 1] == node)
{
path[p - 1].Left = tmp.Left;
}
else
{
path[p - 1].Right = tmp.Left;
}
}
else //當只有左子樹或右子樹或為葉子結點時
{ //首先找到惟一的孩子結點
tmp = node.Left;
if (tmp == null) //如果只有右孩子或沒孩子
{
tmp = node.Right;
}
if (p > 0)
{
if (path[p - 1].Left == node)
{ //如果被刪結點是左孩子
path[p - 1].Left = tmp;
}
else
{ //如果被刪結點是右孩子
path[p - 1].Right = tmp;
}
}
else //當刪除的是根結點時
{
_head = tmp;
}
}
//刪除完後進行旋轉,現在p指向實際被刪除的結點
int data = node.Data;
while (p > 0)
{ //bf表示平衡因子的改變數,當刪除的是左子樹中的結點時,平衡因子-1
//當刪除的是右子樹的孩子時,平衡因子+1
int bf = (data <= path[p - 1].Data) ? -1 : 1;
path[--p].BF += bf; //改變當父結點的平衡因子
bf = path[p].BF; //獲取當前結點的平衡因子
if (bf != 0) //如果bf==0,表明高度降低,繼續後上回溯
{
//如果bf為1或-1則說明高度未變,停止回溯,如果為2或-2,則進行旋轉
//當旋轉後高度不變,則停止回溯
if (bf == 1 || bf == -1 || !RotateSubTree(bf))
{
break;
}
}
}
}
//旋轉以root為根的子樹,當高度改變,則返回true;高度未變則返回false
private bool RotateSubTree(int bf)
{
bool tallChange = true;
Node root = path[p], newRoot = null;
if (bf == 2) //當平衡因子為2時需要進行旋轉操作
{
int leftBF = root.Left.BF;
if (leftBF == -1) //LR型旋轉
{
newRoot = LR(root);
}
else if (leftBF == 1)
{
newRoot = LL(root); //LL型旋轉
}
else //當旋轉根左孩子的bf為0時,只有刪除時才會出現
{
newRoot = LL(root);
tallChange = false;
}
}
if (bf == -2) //當平衡因子為-2時需要進行旋轉操作
{
int rightBF = root.Right.BF; //獲取旋轉根右孩子的平衡因子
if (rightBF == 1)
{
newRoot = RL(root); //RL型旋轉
}
else if (rightBF == -1)
{
newRoot = RR(root); //RR型旋轉
}
else //當旋轉根左孩子的bf為0時,只有刪除時才會出現
{
newRoot = RR(root);
tallChange = false;
}
}
//更改新的子樹根
if (p > 0)
{
if (root.Data < path[p - 1].Data)
{
path[p - 1].Left = newRoot;
}
else
{
path[p - 1].Right = newRoot;
}
}
else
{
_head = newRoot; //如果旋轉根為AVL樹的根,則指定新AVL樹根結點
}
return tallChange;
}
//root為旋轉根,rootPrev為旋轉根雙親結點
private Node LL(Node root) //LL型旋轉,返回旋轉後的新子樹根
{
Node rootNext = root.Left;
root.Left = rootNext.Right;
rootNext.Right = root;
if (rootNext.BF == 1)
{
root.BF = 0;
rootNext.BF = 0;
}
else //rootNext.BF==0的情況,刪除時用
{
root.BF = 1;
rootNext.BF = -1;
}
return rootNext; //rootNext為新子樹的根
}
private Node LR(Node root) //LR型旋轉,返回旋轉後的新子樹根
{
Node rootNext = root.Left;
Node newRoot = rootNext.Right;
root.Left = newRoot.Right;
rootNext.Right = newRoot.Left;
newRoot.Left = rootNext;
newRoot.Right = root;
switch (newRoot.BF) //改變平衡因子
{
case 0:
root.BF = 0;
rootNext.BF = 0;
break;
case 1:
root.BF = -1;
rootNext.BF = 0;
break;
case -1:
root.BF = 0;
rootNext.BF = 1;
break;
}
newRoot.BF = 0;
return newRoot; //newRoot為新子樹的根
}
private Node RR(Node root) //RR型旋轉,返回旋轉後的新子樹根
{
Node rootNext = root.Right;
root.Right = rootNext.Left;
rootNext.Left = root;
if (rootNext.BF == -1)
{
root.BF = 0;
rootNext.BF = 0;
}
else //rootNext.BF==0的情況,刪除時用
{
root.BF = -1;
rootNext.BF = 1;
}
return rootNext; //rootNext為新子樹的根
}
private Node RL(Node root) //RL型旋轉,返回旋轉後的新子樹根
{
Node rootNext = root.Right;
Node newRoot = rootNext.Left;
root.Right = newRoot.Left;
rootNext.Left = newRoot.Right;
newRoot.Right = rootNext;
newRoot.Left = root;
switch (newRoot.BF) //改變平衡因子
{
case 0:
root.BF = 0;
rootNext.BF = 0;
break;
case 1:
root.BF = 0;
rootNext.BF = -1;
break;
case -1:
root.BF = 1;
rootNext.BF = 0;
break;
}
newRoot.BF = 0;
return newRoot; //newRoot為新子樹的根
}
}
執行效果如圖7所示: