引言
在上一篇博文中,介紹了二叉查找樹。在二叉查找樹的基礎上,深入研究一下AVL樹,並用代碼實現核心模塊:插入和刪除。在本篇博文中主要詳細介紹了AVL樹的平衡概念,同時介紹解決平衡問題的旋轉問題。在實現代碼部分詳細介紹在插入的時候保證樹的平衡。筆者目前整理的一些blog針對面試都是超高頻出現的。大家可以點擊鏈接:http://blog.csdn.net/u012403290
技術點
1、AVL樹
通俗來說就是一棵的左子樹和右子樹的高度最多相差1個路徑的二叉查找樹。所以說AVL樹是建立在二叉查找樹之上的,我們知道在二叉查找樹最壞的情況就是每個節點都只有左(右)兒子,導致二叉查找樹變成了一個鏈表,而AVL樹就是爲了克服這個問題產生的。比如說下面兩顆樹:
在左圖中,對於節點5,它的左子樹的高度爲2,右子樹的高度爲1,兩者相差1個路徑,所以它符合AVL樹的定義;在右圖中,對於節點7,它的左子樹高度爲2,右子樹高度爲0,兩者相差2個路徑,所以它不符合AVL樹的定義。
2、旋轉
因爲在插入或者刪除的時候會導致AVL樹變得不平衡,那麼就會採用旋轉來調整樹的新平衡。旋轉分爲兩種,一種是左旋,一種是又旋。節點插入導致的不平衡分爲4種情況,假設我們不平衡的節點爲N:
①、在N的左兒子的左子樹進行一次插入,可以稱爲LL,這個時候可以通過右旋來解決:
注意,在下面的介紹中,不會再繼續標誌節點的左右子樹的高了。我們用平衡因子BF來標誌。我們用|0,1|,絕對值的0和1表示某一個節點的左右子樹的高度差,如果值大於1了,就表示這個節點已經不平衡了。
②、在N的右兒子的右子樹進行一次插入,可以稱爲RR,這個時候可以通過左旋來解決:
③、在N的左兒子的右子樹進行一次插入,可以稱爲LR,這個時候就不可以通過一次旋轉來解決了,而是要先通過左旋,再通過右旋恢復平衡:
在LR的情況中,如果我們按照之前的那麼只能對A節點進行右旋(如果要左旋的話,必須右兒子不爲空),按照上面的規則,把B節點直接拎起來會發現這種情況下的樹就不是二叉查找樹了,因爲新節點必然比B節點要大,如果直接把B拎起來,那麼新節點就會變成了B的左節點,顯然是不行的。所以LR的情況需要進行兩次旋轉,第一次保證支持二叉查找樹,第二次調整平衡。
④、在N的右兒子的左子樹進行一次插入,可以稱爲RL,這個時候和上面一樣需要進行兩次旋轉來解決:
介紹完4種情況,我們做一個總結,方便後面代碼實現:在AVL樹種插入一個節點N,首先我們肯定是要遞歸把N插入到它所合適的節點上(什麼叫合適的節點,在上一篇博文介紹二叉查找樹有說過)。如果插入之後,更新相關節點的高度信息之後,發現沒有超過1,那麼插入就結束。如果發現某一個節點的BF>1了,表示這個節點所代表的的這顆樹已經是不平衡的了,需要按照上面4種情況判斷結果,並進行相依的選轉操作來保證還是一顆AVL樹。
代碼實現AVL樹的插入
分析完AVL樹的性質和它維持平衡的方式,我們對它進行一個簡單的實現:
①必須要有一個類來表示一個節點,且這個節點要包含數據部分,左孩子,右孩子和高。
②在插入的過程中必須要更新被影響的節點的高信息。如果發現節點不平衡需要判斷是屬於哪種情況的不平衡,並採用相應的選擇讓樹恢復平衡。
以下就是實現的代碼:
package com.brickworkers;
public class AvlTree<T extends Comparable<? super T>> {
private static final int MAX_BF = 1;
//一個枚舉類,表示再插入的時候判斷是屬於什麼情況
private static enum Condition{
LL, RR, LR, RL;
}
//靜態內部類表示節點
private static class AvlNode<T>{
T data; //存儲的數據
AvlNode<T> left;//左孩子
AvlNode<T> right;//右孩子
int height;//樹的高度
AvlNode(T data, AvlNode<T> lt, AvlNode<T> rt) {
this.data = data;
left = lt;
right = rt;
height = 0;//新節點插入必然是0高度的
}
AvlNode(T data) {
this(data, null, null);
}
}
//實現插入
private AvlNode<T> insert(T data, AvlNode<T> node){
if(node == null){
return new AvlNode<T>(data);
}
if(data.compareTo(node.data) < 0){
node.left = insert(data, node.left);
}
else if(data.compareTo(node.data) > 0){
node.right = insert(data, node.right);
}
else{
//不能插入一樣的數據
}
return balance(node);//重新調整樹的平衡
}
//重新調整樹的平衡
private AvlNode<T> balance(AvlNode<T> node){
//如果當前節點的左子樹比右子樹大,且超過了1,那麼就有兩種情況,就是LL,或者LR
if(node.left.height - node.right.height > MAX_BF){
if(node.left.left.height >= node.left.right.height){//LL情況
singleRotate(node, Condition.LL);
}else{//否則就是LR情況,就需要進行雙旋轉
doubleRotate(node, Condition.LR);
}
}
//如果當前節點的右子樹比左子樹大,且超過了1,那麼就有兩種情況,就是RR,或者RL
else if(node.right.height - node.left.height > MAX_BF){
if(node.right.right.height >= node.right.left.height){//RR情況
singleRotate(node, Condition.RR);
}else{//否則就是RL情況
doubleRotate(node, Condition.RL);
}
}
//重新修正一下當前節點的高度
node.height = Math.max(node.left.height, node.right.height) + 1;
return node;
}
//實現單旋轉
private AvlNode<T> singleRotate(AvlNode<T> node, Condition rotateCondition){
AvlNode<T> pullNode;
if(rotateCondition == Condition.RR){//如果是左旋操作
//找準前面我們說的要拎起來的點
pullNode = node.right;//按我們前面說的就是:左旋,把不平衡節點的右節點拎起來
node.right = pullNode.left;//先處理不平衡節點,如果pull節點本身就有左節點,要把左邊節點交給node作爲它的右節點
pullNode.left = node;//puu節點的左節點變成了node
//調整節點高度
node.height = Math.max(node.left.height, node.right.height) + 1;//旋轉結束之後,需要重新計算變動節點的高度
pullNode.height = Math.max(node.height, pullNode.right.height) + 1;
}else{//右旋操作
//方式方法和左旋是一樣的就不再贅述
pullNode = node.left;
node.left = pullNode.right;
pullNode.right = node;
node.height = Math.max(node.left.height, node.right.height) + 1;
pullNode.height = Math.max(pullNode.left.height, node.height) + 1;
}
return pullNode;
}
//實現雙旋轉,雙旋轉其實就是拆分爲兩個旋轉方式。
private AvlNode<T> doubleRotate(AvlNode<T> node, Condition rotateCondition){
if(rotateCondition == Condition.RL){//如果是RL,就需要先右旋再左旋
node.right = singleRotate(node.right, Condition.LL);//先進行一次右旋
return singleRotate(node, Condition.RR);//再進行一次左旋
}else{
node.left = singleRotate(node.left, Condition.RR);//先進行一次左旋
return singleRotate(node, Condition.LL);//在進行一次右旋
}
}
}
這裏實現了核心的旋轉和插入,用一個內部枚舉類控制當插入之後,重新平衡的時候用於標記當前屬於什麼情況,該調用什麼方法。AVL節點內部類用於表示樹中的每一個節點,這個節點包含了AVL特有的高度信息。當然了,在刪除的過程也會導致樹變成不平衡的,大家有興趣可以建立在上一篇博文二叉查找樹的博文之上然後參考上面的實現,對刪除方法進行修改。不過在源代碼中,我遺漏了一點,如果某一個計算一個空節點的高,那麼應該返回-1,在最上面的圖中我已經解釋了爲什麼。大家自己加入到上面的代碼中,進行補充。
如果博文存在什麼問題,或者想法請留言,希望對大家有所幫助。