【資料結構】平衡二叉樹之AVL樹
平衡二叉排序樹
平衡二叉排序樹(Balanced Binary Sort Tree),上一篇部落格【資料結構】二叉排序樹BST講了BST,並且在最後我們說BST上的操作不會超過O(h),既然樹高這麼重要,那麼BBST的研究就是為了使得樹的深度在可接受的範圍內漸近意義下達到O(lgn)
n個節點組成的二叉樹,其高度為lgn取下限時,這棵樹是理想平衡的,滿足這樣條件的樹只有完全二叉樹和滿二叉樹,這樣的要求未免太苛刻,並且實際中沒有意義。
適度平衡:保證樹高在漸近意義上不超過O(lgn)適度平衡的樹也稱作平衡二叉樹。
並且我們知道,任何平衡的二叉樹在經過一系列的插入刪除操作後可能會不平衡,因此任何一種BBST都包含了界定平衡的標準和一系列重平衡(rebalance)的方法。
平衡二叉排序樹包括:AVL樹,紅黑樹,伸展樹等
AVL樹
AVL樹僅僅是平衡二叉樹的一種,它是最經典的最早發明的一種BBST(貌似嚴蔚敏老師的書上說平衡二叉樹又稱AVL樹),AVL樹由G.M. Adelson-Velsky 和 E.M. Landis發明。AVL定義適度平衡的標準:
定義平衡因子(BalanceFactor):BF(node) = height(lchild(node)) - height(rchild(node))
對所有的節點node,有|BF(node)| <= 1
一系列的重平衡方法:旋轉操作
基本的旋轉操作如下圖所示
根據不同的情形又分為四種情況
相關演算法
1.返回某個結點的高度height我們設定當結點為null時的高度為-1,其餘情況的高度由每個結點自己維護。
private int height(BinaryTreeNode node) {
if (node == null) {
return -1;
} else {
return node.height;
}
}
2.返回某個結點的平衡因子
平衡因子等於左子樹高度減右子樹高度
private int balance(BinaryTreeNode node) { if (node == null) return 0; return height(node.lchild) - height(node.rchild); }
3.左旋LL
按照圖示順序分三步,將tmp作為最終的根結點返回。
需要額外做的工作包括對父結點改變了的結點賦新的parent,更新高度,最終返回的結點的父親節點一定要在呼叫處設定!!// LL 將node結點左旋,node是最小不平衡子樹的根
public BinaryTreeNode leftRotate(BinaryTreeNode node) {
BinaryTreeNode tmp = node.lchild;
tmp.parent = null;
node.lchild = tmp.rchild;
if (tmp.rchild != null)
tmp.rchild.parent = node;
tmp.rchild = node;
node.parent = tmp;
// 更新高度,包括是node和node.rchild
node.height = Math.max(height(node.lchild), height(node.rchild)) + 1;
tmp.height = Math.max(height(tmp.lchild), node.height) + 1;
return tmp;
}
4.雙旋LR
分兩步即可,先對結點的左子樹進行左旋操作,再對該結點進行右旋操作,要注意儲存第一次返回的結點的父親結點!!
public BinaryTreeNode leftRightRotate(BinaryTreeNode node) {
node.lchild = rightRotate(node.lchild);
node.lchild.parent = node; // 父結點賦值!!
return leftRotate(node);
}
5.返回待旋轉的結點的旋轉型別
private RotateType rotateType(BinaryTreeNode node) {
if (balance(node) < -1) {
if (balance(node.rchild) > 0) {
return RotateType.RL;
} else {
return RotateType.RR;
}
} else { // >1
if (balance(node.lchild) < 0) {
return RotateType.LR;
} else {
return RotateType.LL;
}
}
}
6.用以node為頂點旋轉過後的樹來代替node這棵子樹
區別之前用的replace函式void replace(BinaryTreeNode node1, BinaryTreeNode node2)
將結點A旋轉後,A的父結點左右孩子都已經變了,因此要先將他的父結點儲存下來以便將旋轉後的子樹往它後面掛,所以對該函式的呼叫不等於呼叫replace(node,leftRotate(node),type);
// 拼接旋轉後的子樹:用以node為定點旋轉過後的樹來代替node這棵子樹
private void replaceNode(BinaryTreeNode node,
RotateType type) {
BinaryTreeNode tmpParent = node.parent;
BinaryTreeNode rotateNode = null;
switch (type) {
case LL:
rotateNode = leftRotate(node);
break;
case RR:
rotateNode = rightRotate(node);
break;
case LR:
rotateNode = leftRightRotate(node);
break;
case RL:
rotateNode = rightLeftRotate(node);
break;
}
if (tmpParent == null) {
root = rotateNode;
rotateNode.parent = null; //父結點賦值非常關鍵!!
} else if (tmpParent.rchild == node) {
tmpParent.rchild = rotateNode;
rotateNode.parent = root; //父結點賦值非常關鍵!!
} else {
tmpParent.lchild = rotateNode;
rotateNode.parent = root; //父結點賦值非常關鍵!!
}
}
插入和刪除
有了上面一些基本的函式,就可以進行下面的兩個重要操作:插入/刪除結點
插入節點
當插入一個結點可能會造成他的祖父結點失衡,進而曾祖父結點也會失衡等等一直往上延伸。這麼看來插入操作似乎很複雜?事實並不是這樣,儘管這樣,我們卻不需要從下往上挨個調整結點至平衡。因為好訊息是隻要把從下往上的第一個失衡的結點所構成的子樹重平衡就能保證其之上的所有結點平衡。這裡的第一個失衡的結點所構成的子樹稱為最小不平衡子樹。調整最小不平衡子樹至重平衡,它的高度和插入結點之前相比未變,因此也不會影響到更上層的結點。這樣只需要有限步操作即可重平衡,複雜度為O(1)。這裡每插入一個節點都要更新已存在的結點的高度!插入的實現和BST裡面一樣,只是要更新結點高度
private BinaryTreeNode insertRecurAVL(BinaryTreeNode root,BinaryTreeNode insertNode) {
if (this.root == null) {
this.root = root = insertNode;
} else if (insertNode.data < root.data && root.lchild == null) {
root.lchild = insertNode;
insertNode.parent = root;
} else if (insertNode.data >= root.data && root.rchild == null) {
root.rchild = insertNode;
insertNode.parent = root;
} else {
if (insertNode.data < root.data && root.lchild != null)
insertRecurAVL(root.lchild, insertNode);
if (insertNode.data >= root.data && root.rchild != null)
insertRecurAVL(root.rchild, insertNode);
}
// 更新高度!
root.height = Math.max(height(root.lchild), height(root.rchild)) + 1;// 放在這裡的位置很重要
return insertNode;
}
於是插入節點後對其進行檢查重平衡的程式碼如下,基本思路是將新的結點插入到對應的位置,然後從他開始往上走,直到遇到第一個不平衡的結點,也就是最小不平衡子樹,然後旋轉這個結點讓其恢復平衡後來替換該結點。
public void insertAVL(BinaryTreeNode insertNode) {
BinaryTreeNode node = insertRecurAVL(root, insertNode);
// 調整最小不平衡子樹
while (node != null && balance(node) > -2 && balance(node) < 2) {
node = node.parent;
} // 跳出迴圈時,node為null 或者node不平衡
if (node != null) {
// 確定何種型別的旋轉
RotateType type = rotateType(node);
replaceNode(node, type);
}
}
從陣列建立一棵BBST的過程也可以通過insert操作迴圈的插入即可。刪除結點
刪除某個節點只可能導致他的父親節點失衡,因為,如果我們刪除最深的那條子樹,那麼不會失衡,所以產生失衡只可能是由於刪除了短的子樹上的結點,這樣對外界來說,該結點的父親所在的子樹的高度未變,於是上面的結點的平衡性也不會改變。那麼刪除操作只需要重平衡它的父結點嗎?事實上,刪除一個結點他的父親如果發生了失衡,那麼當讓其父親節點重平衡後,區域性子樹的高度減少了,因此失衡的情況可能繼續往上傳遞,最差情況下一直傳遞到根,於是刪除的複雜度為O(lgn)。其實也就是因為這個原因,AVL樹用的不多,SGI的STL中都未實AVL樹,僅僅實現了紅黑樹
// 刪除AVL樹中的結點
public void deleteAVL(BinaryTreeNode node) {
System.out.println(node.toString());
BinaryTreeNode predecessorOfnode = null;
if (node.lchild == null) { // 左子樹為空,只需要移植右子樹
replace(node, node.rchild);
} else if (node.rchild == null) {
replace(node, node.lchild);
} else {
predecessorOfnode = predecessor(node);
replace(node, node.lchild);
predecessorOfnode.rchild = node.rchild;
node.rchild.parent = predecessorOfnode;
predecessorOfnode.height = Math.max(height(predecessorOfnode.lchild), height(node.rchild)) + 1;
}
// 調整平衡
// 只需要從刪除的結點的前驅開始依次向上判斷
BinaryTreeNode nodetmp = predecessorOfnode;
while (nodetmp != null) {
BinaryTreeNode tmp = nodetmp.parent; // 下面的旋轉操作會改變nodetmp的父結點,所以提前儲存下來!!
if (balance(nodetmp) < -1 || balance(nodetmp) > 1) {
// 不平衡
RotateType type = rotateType(nodetmp);
replaceNode(nodetmp, type);
}
nodetmp = tmp;
}
}
AVL樹的實現
其中使用的BinaryTreeNode比起之前多了一個height欄位,用來儲存每個節點的高度,結點為null時,高度為-1
public class AVLTree extends BinarySearchTree {
public enum RotateType {
LL, RR, LR, RL
};
@Override
public void createTree(int[] array) {
// 從一個數組建立二叉搜尋樹
for (int i : array) {
insertAVL(new BinaryTreeNode(i));
}
}
// 刪除AVL樹中的結點
public void deleteAVL(BinaryTreeNode node) {
BinaryTreeNode predecessorOfnode = null;
if (node.lchild == null) { // 左子樹為空,只需要移植右子樹
replace(node, node.rchild);
} else if (node.rchild == null) {
replace(node, node.lchild);
} else {
predecessorOfnode = predecessor(node);
replace(node, node.lchild);
predecessorOfnode.rchild = node.rchild;
node.rchild.parent = predecessorOfnode;
predecessorOfnode.height = Math.max(
height(predecessorOfnode.lchild), height(node.rchild)) + 1;
}
// 調整平衡
// 只需要從刪除的結點的前驅開始依次向上判斷
BinaryTreeNode nodetmp = predecessorOfnode;
while (nodetmp != null) {
BinaryTreeNode tmp = nodetmp.parent; // 下面的旋轉操作會改變nodetmp的父結點,所以提前儲存下來!!
if (balance(nodetmp) < -1 || balance(nodetmp) > 1) {
// 不平衡
RotateType type = rotateType(nodetmp);
replaceNode(nodetmp, type);
}
nodetmp = tmp;
}
}
// 插入節點 並處理可能的不平衡結點
public void insertAVL(BinaryTreeNode insertNode) {
BinaryTreeNode node = insertRecurAVL(root, insertNode);
while (node != null && balance(node) > -2 && balance(node) < 2) {
node = node.parent;
} // 跳出迴圈時,node為null 或者node不平衡
if (node != null) {
// 確定何種型別的旋轉
RotateType type = rotateType(node);
replaceNode(node, type);
}
}
// 遞迴的插入結點,同時更新每個結點的高度
private BinaryTreeNode insertRecurAVL(BinaryTreeNode root,
BinaryTreeNode insertNode) {
if (this.root == null) {
this.root = root = insertNode;
} else if (insertNode.data < root.data && root.lchild == null) {
root.lchild = insertNode;
insertNode.parent = root;
} else if (insertNode.data >= root.data && root.rchild == null) {
root.rchild = insertNode;
insertNode.parent = root;
} else {
if (insertNode.data < root.data && root.lchild != null)
insertRecurAVL(root.lchild, insertNode);
if (insertNode.data >= root.data && root.rchild != null)
insertRecurAVL(root.rchild, insertNode);
}
root.height = Math.max(height(root.lchild), height(root.rchild)) + 1;// 放在這裡的位置很重要
return insertNode;
}
// 拼接旋轉後的子樹:用以node為定點旋轉過後的樹來代替node這棵子樹
private void replaceNode(BinaryTreeNode node, RotateType type) {
BinaryTreeNode tmpParent = node.parent;
BinaryTreeNode rotateNode = null;
switch (type) {
case LL:
rotateNode = leftRotate(node);
break;
case RR:
rotateNode = rightRotate(node);
break;
case LR:
rotateNode = leftRightRotate(node);
break;
case RL:
rotateNode = rightLeftRotate(node);
break;
}
if (tmpParent == null) {
root = rotateNode;
rotateNode.parent = null; // 父結點賦值非常關鍵!!
} else if (tmpParent.rchild == node) {
tmpParent.rchild = rotateNode;
rotateNode.parent = root; // 父結點賦值非常關鍵!!
} else {
tmpParent.lchild = rotateNode;
rotateNode.parent = root; // 父結點賦值非常關鍵!!
}
}
// 獲取待旋轉結點的旋轉型別
private RotateType rotateType(BinaryTreeNode node) {
if (balance(node) < -1) {
if (balance(node.rchild) > 0) {
return RotateType.RL;
} else {
return RotateType.RR;
}
} else { // >1
if (balance(node.lchild) < 0) {
return RotateType.LR;
} else {
return RotateType.LL;
}
}
}
// 返回結點的平衡因子 返回值為-2 -1 0 1 2
private int balance(BinaryTreeNode node) {
if (node == null)
return 0;
return height(node.lchild) - height(node.rchild);
}
// 返貨某個節點的高度
private int height(BinaryTreeNode node) {
if (node == null) {
return -1;
} else {
return node.height;
}
}
// 將node結點左旋,node是最小不平衡子樹的根
// RR
public BinaryTreeNode rightRotate(BinaryTreeNode node) {
BinaryTreeNode tmp = node.rchild;
tmp.parent = null;
node.rchild = tmp.lchild;
if (tmp.lchild != null)
tmp.lchild.parent = node;
tmp.lchild = node;
node.parent = tmp;
// 更新高度,包括是node和node.rchild
node.height = Math.max(height(node.lchild), height(node.rchild)) + 1;
tmp.height = Math.max(height(tmp.rchild), node.height) + 1;
return tmp;
}
// 將node結點左旋,node是最小不平衡子樹的根
// LL
public BinaryTreeNode leftRotate(BinaryTreeNode node) {
BinaryTreeNode tmp = node.lchild;
tmp.parent = null;
node.lchild = tmp.rchild;
if (tmp.rchild != null)
tmp.rchild.parent = node;
tmp.rchild = node;
node.parent = tmp;
// 更新高度,包括是node和node.rchild
node.height = Math.max(height(node.lchild), height(node.rchild)) + 1;
tmp.height = Math.max(height(tmp.lchild), node.height) + 1;
return tmp;
}
// LR
public BinaryTreeNode leftRightRotate(BinaryTreeNode node) {
node.lchild = rightRotate(node.lchild);
node.lchild.parent = node;
return leftRotate(node);
}
// RL
public BinaryTreeNode rightLeftRotate(BinaryTreeNode node) {
node.rchild = leftRotate(node.rchild);
node.rchild.parent = node;
return rightRotate(node);
}
}
測試
仍然使用上篇部落格中建立BST的陣列int[] array = { 1, 9, 2, 7, 4, 5, 3, 6, 8 };
public static void main(String[] args) {
AVLTree avl = new AVLTree();
int[] array = { 1, 9, 2, 7, 4, 5, 3, 6, 8 };
avl.createTree(array);
System.out.println("層序列印結點和其高度");
avl.levelOrderH();
System.out.println("刪除結點7得到的層序遍歷");
avl.deleteAVL(avl.search(avl.root, 7));
avl.levelOrderH();
}
輸出
層序列印結點和其高度
4
2 7
1 3 5 9
6 8
刪除結點7得到的層序遍歷
4
2 8
1 3 5 9
6
可以發現,前面將該陣列建立成BST,樹高非常高,現在將其建立成AVL樹,發現非常的平衡,樹高比之前低多了。並且在動態插入和刪除過程中始終能維護樹的平衡性。分析
分析該陣列的建立和刪除結點的過程
上述的程式碼在通過該陣列建立AVL樹的過程如下
刪除結點7的過程如下圖
後記
可能最後的程式碼才200行左右,但是編些難度不小,比起寫個應用分分鐘上千行程式碼難多了,改bug改了一個晚上。由於我在這裡每個節點儲存父結點,所以好幾次忘記給父結點賦值,旋轉的時候,由於指標變化也會產生好多問題,編寫程式碼的過程中遇到導致錯誤的地方都標註在程式碼中。