1. 什麼AVL樹
AVL樹由兩位科學家在1962年發表的論文《An algorithm for the organization of information》當中提出,其命名來自於它的發明者G.M. Adelson-Velsky和E.M. Landis的名字縮寫。
AVL樹是最先發明的自平衡二叉查找樹,也被稱爲高度平衡樹。相比於二叉查找樹,它的特點是:任何節點的兩個子樹的最大高度差爲1。
上面的兩張圖片,左邊的是AVL樹,它的任何節點的兩個子樹的高度差別都<=1;而右邊的不是AVL樹,因爲7的兩顆子樹的高度相差爲2(以2爲根節點的樹的高度是3,而以8爲根節點的樹的高度是1)。
2. AVL樹的作用
對於一般的二叉搜索樹,其期望高度(即爲一棵平衡樹時)爲log2n,其各操作的時間複雜度O(log2n)同時也由此而決定。但是,在某些極端的情況下(如在插入的序列是有序的時),二叉搜索樹將退化成近似鏈或鏈,此時,其操作的時間複雜度將退化成線性的,即O(n)。我們可以通過隨機化建立二叉搜索樹來儘量的避免這種情況,但是在進行了多次的操作之後,由於在刪除時,我們總是選擇將待刪除節點的後繼代替它本身,這樣就會造成總是右邊的節點數目減少,以至於樹向左偏沉。這同時也會造成樹的平衡性受到破壞,提高它的操作的時間複雜度。
例如:我們按順序將一組數據1,2,3,4,5,6分別插入到一顆空二叉查找樹和AVL樹中,插入的結果如下圖:
由上圖可知,同樣的結點,由於插入方式不同導致樹的高度也有所不同。特別是在帶插入結點個數很多且正序的情況下,會導致二叉樹的高度是O(N),而AVL樹就不會出現這種情況,樹的高度始終是O(lgN)。高度越小,對樹的一些基本操作的時間複雜度就會越小。
AVL樹的操作基本和二叉查找樹一樣,我們關注的是兩個變化很大的操作:插入和刪除。
AVL樹不僅是一顆二叉查找樹,它還有其他的性質。如果我們按照一般的二叉查找樹的插入方式可能會破壞AVL樹的平衡性。同理,在刪除的時候也有可能會破壞樹的平衡性,所以我們要做一些特殊的旋轉處理來重新恢復平衡。
3. 旋轉
如果在AVL樹中進行插入或刪除節點後,可能導致AVL樹失去平衡。這種失去平衡的可以概括爲4種姿態:LL(左左),LR(左右),RR(右右)和RL(右左)。下面給出它們的示意圖:
上圖中的4棵樹都是"失去平衡的AVL樹",從左往右的情況依次是:LL、LR、RL、RR。除了上面的情況之外,還有其它的失去平衡的AVL樹,如下圖:
上面的兩張圖都是爲了便於理解,而列舉的關於"失去平衡的AVL樹"的例子。總的來說,AVL樹失去平衡時的情況一定是LL、LR、RL、RR這4種之一,它們都由各自的定義:
- (1)LL:LeftLeft,也稱爲"左左"。插入或刪除一個節點後,根節點的左子樹的左子樹還有非空子節點,導致"根的左子樹的高度"比"根的右子樹的高度"大2,導致AVL樹失去了平衡。
例如,在上面LL情況中,由於"根節點(8)的左子樹(4)的左子樹(2)還有非空子節點",而"根節點(8)的右子樹(12)沒有子節點";導致"根節點(8)的左子樹(4)高度"比"根節點(8)的右子樹(12)"高2。 - (2) LR:LeftRight,也稱爲"左右"。插入或刪除一個節點後,根節點的左子樹的右子樹還有非空子節點,導致"根的左子樹的高度"比"根的右子樹的高度"大2,導致AVL樹失去了平衡。
例如,在上面LR情況中,由於"根節點(8)的左子樹(4)的左子樹(6)還有非空子節點",而"根節點(8)的右子樹(12)沒有子節點";導致"根節點(8)的左子樹(4)高度"比"根節點(8)的右子樹(12)"高2。 - (3) RL:RightLeft,稱爲"右左"。插入或刪除一個節點後,根節點的右子樹的左子樹還有非空子節點,導致"根的右子樹的高度"比"根的左子樹的高度"大2,導致AVL樹失去了平衡。
例如,在上面RL情況中,由於"根節點(8)的右子樹(12)的左子樹(10)還有非空子節點",而"根節點(8)的左子樹(4)沒有子節點";導致"根節點(8)的右子樹(12)高度"比"根節點(8)的左子樹(4)"高2。 - (4) RR:RightRight,稱爲"右右"。插入或刪除一個節點後,根節點的右子樹的右子樹還有非空子節點,導致"根的右子樹的高度"比"根的左子樹的高度"大2,導致AVL樹失去了平衡。
例如,在上面RR情況中,由於"根節點(8)的右子樹(12)的右子樹(14)還有非空子節點",而"根節點(8)的左子樹(4)沒有子節點";導致"根節點(8)的右子樹(12)高度"比"根節點(8)的左子樹(4)"高2。
如果在AVL樹中進行插入或刪除節點後,可能導致AVL樹失去平衡。AVL失去平衡之後,可以通過旋轉使其恢復平衡,下面分別介紹"LL(左左),LR(左右),RR(右右)和RL(右左)"這4種情況對應的旋轉方法。
3.1 LL的旋轉
LL失去平衡的情況,可以通過一次旋轉讓AVL樹恢復平衡。如下圖:
圖中左邊是旋轉之前的樹,右邊是旋轉之後的樹。從中可以發現,旋轉之後的樹又變成了AVL樹,而且該旋轉只需要一次即可完成。
對於LL旋轉,你可以這樣理解爲:LL旋轉是圍繞"失去平衡的AVL根節點"進行的,也就是節點k2;而且由於是LL情況,即左左情況,就用手抓着"左孩子,即k1"使勁搖。將k1變成根節點,k2變成k1的右子樹,“k1的右子樹"變成"k2的左子樹”。
3.2 RR的旋轉
理解了LL之後,RR就相當容易理解了。RR是與LL對稱的情況!RR恢復平衡的旋轉方法如下:
圖中左邊是旋轉之前的樹,右邊是旋轉之後的樹。RR旋轉也只需要一次即可完成。
3.3 LR的旋轉
LR失去平衡的情況,需要經過兩次旋轉才能讓AVL樹恢復平衡。如下圖:
第一次旋轉是圍繞"k1"進行的"RR旋轉",第二次是圍繞"k3"進行的"LL旋轉"。
3.4 RL的旋轉
RL是與LR的對稱情況!RL恢復平衡的旋轉方法如下:
第一次旋轉是圍繞"k3"進行的"LL旋轉",第二次是圍繞"k1"進行的"RR旋轉"。
4. java實現
/**
* @author chenlongfei
*/
public class AVLTree {
private AVLTreeNode root; // 根結點
/**
* 插入操作的入口
* @author chenlongfei
* @param insertValue
*/
public void insert(long insertValue) {
root = insert(root, insertValue);
}
/**
* 插入的地遞歸實現
* @author chenlongfei
* @param subTree
* @param insertValue
* @return
*/
private AVLTreeNode insert(AVLTreeNode subTree, long insertValue) {
if (subTree == null) {
return new AVLTreeNode(insertValue, null, null);
}
if (insertValue < subTree.value) { // 插入左子樹
subTree.left = insert(subTree.left, insertValue);
if (unbalanceTest(subTree)) { // 插入後造成失衡
if (insertValue < subTree.left.value) { // LL型失衡
subTree = leftLeftRotation(subTree);
} else { // LR型失衡
subTree = leftRightRotation(subTree);
}
}
} else if (insertValue > subTree.value) { // 插入右子樹
subTree.right = insert(subTree.right, insertValue);
if (unbalanceTest(subTree)) { // 插入後造成失衡
if (insertValue < subTree.right.value) { // RL型失衡
subTree = rightLeftRotation(subTree);
} else { // RR型失衡
subTree = rightRightRotation(subTree);
}
}
} else {
throw new RuntimeException("duplicate value: " + insertValue);
}
return subTree;
}
/**
* RL型旋轉
* @author chenlongfei
* @param k1 子樹根節點
* @return
*/
private AVLTreeNode rightLeftRotation(AVLTreeNode k1) {
k1.right = leftLeftRotation(k1.right);
return rightRightRotation(k1);
}
/**
* RR型旋轉
* @author chenlongfei
* @param k1 k1 子樹根節點
* @return
*/
private AVLTreeNode rightRightRotation(AVLTreeNode k1) {
AVLTreeNode k2;
k2 = k1.right;
k1.right = k2.left;
k2.left = k1;
return k2;
}
/**
* LR型旋轉
* @author chenlongfei
* @param k3
* @return
*/
private AVLTreeNode leftRightRotation(AVLTreeNode k3) {
k3.left = rightRightRotation(k3.left);
return leftLeftRotation(k3);
}
/**
* LL型旋轉
* @author chenlongfei
* @param k2
* @return
*/
private AVLTreeNode leftLeftRotation(AVLTreeNode k2) {
AVLTreeNode k1;
k1 = k2.left;
k2.left = k1.right;
k1.right = k2;
return k1;
}
/**
* 獲取樹的深度
* @author chenlongfei
* @param treeRoot 根節點
* @param initDeep 初始深度
* @return
*/
private static int getDepth(AVLTreeNode treeRoot, int initDeep) {
if (treeRoot == null) {
return initDeep;
}
int leftDeep = initDeep;
int rightDeep = initDeep;
if (treeRoot.left != null) {
leftDeep = getDepth(treeRoot.left, initDeep++);
}
if (treeRoot.right != null) {
rightDeep = getDepth(treeRoot.right, initDeep++);
}
return Math.max(leftDeep, rightDeep);
}
/**
* 判斷是否失衡
* @author chenlongfei
* @param treeRoot
* @return
*/
private boolean unbalanceTest(AVLTreeNode treeRoot) {
int leftHeight = getDepth(treeRoot.left, 1);
int righHeight = getDepth(treeRoot.right, 1);
int diff = Math.abs(leftHeight - righHeight);
return diff > 1;
}
/**
* 刪除操作的入口
* @param value
*/
public void remove(long value) {
root = remove(root, value);
}
/**
* 刪除操作的遞歸實現
* @param tree
* @param value
* @return
*/
private AVLTreeNode remove(AVLTreeNode tree, long value) {
if (tree == null) {
return tree;
}
if (value < tree.value) { //要刪除的節點在左子樹
tree.left = remove(tree.left, value);
} else if (value > tree.value){ //要刪除的節點在右子樹
tree.right = remove(tree.right, value);
} else if (tree.value == value) { //要刪除的節點就是本身
if (tree.left != null && tree.right != null) { // 左右子樹都存在
if (getDepth(tree.left, 1) > getDepth(tree.right, 1)) {
/**
* 如果tree的左子樹比右子樹高:
* 1. 找出tree的左子樹中的最大節點
* 2. 將該最大節點的值賦值給tree。
* 3. 刪除該最大節點。
* 這類似於用"tree的左子樹中最大節點"做"tree"的替身
* 採用這種方式的好處是:刪除"tree的左子樹中最大節點"之後,AVL樹仍然是平衡的
*/
AVLTreeNode max = getMaxNode(tree.left);
tree.value = max.value;
tree.left = remove(tree.left, max.value);
} else {
/**
* 如果tree的左子樹不高於右子樹:
* 1. 找出tree的右子樹中的最小節點
* 2. 將該最小節點的值賦值給tree。
* 3. 刪除該最小節點。
* 這類似於用"tree的右子樹中最小節點"做"tree"的替身
* 採用這種方式的好處是:刪除"tree的左子樹中最大節點"之後,AVL樹仍然是平衡的
*/
AVLTreeNode min = getMinNode(tree.right);
tree.value = min.value;
tree.right = remove(tree.right, min.value);
}
} else {
tree = tree.left == null ? tree.right : tree.left;
}
} else {
System.out.println("no node matched value: " + value);
}
return tree;
}
/**
* 獲取值最大的節點
* @param node
* @return
*/
private AVLTreeNode getMaxNode(AVLTreeNode node) {
if (node == null) {
return null;
}
if (node.right != null) {
return getMaxNode(node.right);
} else {
return node;
}
}
/**
* 獲取值最小的節點
* @param node
* @return
*/
private AVLTreeNode getMinNode(AVLTreeNode node) {
if (node == null) {
return null;
}
if (node.left != null) {
return getMinNode(node.left);
} else {
return node;
}
}
// AVL樹的節點
class AVLTreeNode {
long value; // 節點存儲的數值
AVLTreeNode left; // 左孩子
AVLTreeNode right; // 右孩子
public AVLTreeNode(long value, AVLTreeNode left, AVLTreeNode right) {
this.value = value;
this.left = left;
this.right = right;
}
public long getValue() {
return this.value;
}
public void setValue(long value) {
this.value = value;
}
public AVLTreeNode getLeft() {
return this.left;
}
public void setLeft(AVLTreeNode left) {
this.left = left;
}
public AVLTreeNode getRight() {
return this.right;
}
public void setRight(AVLTreeNode right) {
this.right = right;
}
}
}
測試代碼:
/**
* 前序遍歷
* @param currentRoot
*/
public static void preorder(AVLTreeNode currentRoot) {
if (currentRoot != null) {
System.out.print(currentRoot.value + "\t");
preorder(currentRoot.left);
preorder(currentRoot.right);
}
}
public static void main(String [] args) {
AVLTree tree = new AVLTree();
int arr[]= {3,2,1,4,5,6,7,16,15,14,13,12,11,10,8,9};
for (int a : arr) {
tree.insert(a);
}
preorder(tree.root);
}
打印結果如下:
3 2 1 4 5 6 7 16 15 14 13 12 11 10 8 9