平衡二叉樹和AVL
1 概述
對於一棵二分搜尋樹,如果我們的資料是順序新增到二分搜尋樹中,它就會退化成一個連結串列。我們如何解決這個問題呢,我們需要在現有的二分搜尋樹的基礎上新增一些機制,使得我們二分搜尋樹能維持平衡二叉樹這樣的一個性質,而AVL就是一種最為經典的平衡二叉樹。
什麼樣的樹才是平衡二叉樹呢?
滿二叉數:除了葉子節點,其餘節點都有左右子樹,所以是一棵平衡二叉樹
而最大堆中所說的完全二叉樹:因為它葉子節點中最大深度值與最小深度值相差不超過1,也是平衡二叉樹。、
我們在AVL中所維護的這種平衡二叉樹,它的條件更加寬鬆些:對於任意一個節點,左子樹和右子樹的高度差不能超過1
現在我們按照二分搜尋樹新增元素的方法往上圖中的樹中新增2和7兩個元素,可得下圖
我們再來計算各個節點的高度和平衡因子
可以看到,根節點12和它的左孩子節點8的平衡因子都為2,所以此時就不是平衡二叉樹了。
2. 新增節點(4種情況)
我們要注意,AVL樹只是對二分搜尋樹的改進,它本質上還是一個二分搜尋樹,所以我們只需要在二分搜尋樹程式碼的基礎上進行修改就可以了。二分搜尋樹的程式碼在我github上:GitHub連結
由於我們只有新新增一個節點,才會導致以前的平衡二叉樹可能變為不平衡,而不平衡節點的出現只可能出現在插入節點(葉子節點)的父輩節點上。所以我們維護平衡的時機應該是我們加入節點之後,我們沿著這個節點向上回溯來維持這個平衡性,由於我們新增節點的操作時遞迴進行的,所以我們來維護也是很容易的。
維護時的情況有四種,LL,RR,LR,RL
1) LL——右旋轉(插入的元素在不平衡節點的左側的左側)
我們來看個例子:
上面這個圖是個平衡二叉樹,現在往這個平衡二叉樹新增一個節點2:
可以看到,此時節點8左子樹高度為3,右子樹高度為1,所以節點8的平衡因子為2,所以節點8打破了平衡二叉樹的性質,所以我們需要對節點8這個位置進行一個平衡的維護,那麼我們怎麼來維護呢?
上面的情況我們可以抽象成下圖中的左邊,我們分兩步,第一步:將y連線在x節點的右孩子上;第二部:將T3連線在y的左孩子上;這樣就變成下圖右邊的樣子了,這樣既滿足了二分搜尋樹的性質,又滿足了平衡二叉樹的性質
2) RR——左旋轉(插入的元素在不平衡節點的右側的右側)
上面兩種情況的程式碼:
// 向二分搜尋樹中新增新的元素(key, value)
public void add(K key, V value){
root = add(root, key, value);
}
private Node add(Node node, K key, V value){
if(node == null){
size ++;
return new Node(key, value);
}
if(key.compareTo(node.key) < 0)
node.left = add(node.left, key, value);
else if(key.compareTo(node.key) > 0)
node.right = add(node.right, key, value);
else // key.compareTo(node.key) == 0
node.value = value;
// 更新height
node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
// 計算平衡因子
int balanceFactor = getBalanceFactor(node);
// 平衡維護
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)
return rightRotate(node);
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)
return leftRotate(node);
return node;
}
// 對節點y進行向右旋轉操作,返回旋轉後新的根節點x
// y x
// / \ / \
// x T4 向右旋轉 (y) z y
// / \ - - - - - - - -> / \ / \
// z T3 T1 T2 T3 T4
// / \
// T1 T2
private Node rightRotate(Node y) {
Node x = y.left;
Node T3 = x.right;
// 向右旋轉過程
x.right = y;
y.left = T3;
// 更新height
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
// 對節點y進行向左旋轉操作,返回旋轉後新的根節點x
// y x
// / \ / \
// T1 x 向左旋轉 (y) y z
// / \ - - - - - - - -> / \ / \
// T2 z T1 T2 T3 T4
// / \
// T3 T4
private Node leftRotate(Node y) {
Node x = y.right;
Node T2 = x.left;
// 向左旋轉過程
x.left = y;
y.right = T2;
// 更新height
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
3) LR——(插入的元素在不平衡節點的左側的右側)
這種情況如下圖:
我們看下上圖的左邊,如果我們現在使用右旋轉,則12變成8的左孩子,10變成8的右孩子,違反了二分搜尋樹的原則。所以我們將上面的情況抽象:
此時,我們先對x進行左旋轉
可以看到,此時已經轉換成LL的情況了,然後我們再按第一節右旋轉的方法對y節點進行右旋轉就可以啦
4) RL——(插入的元素在不平衡節點的右側的左側)
先對x進行右旋轉
然後再對y進行左旋轉就可以了
對於程式碼方面,第三四種情況直接服用左旋轉和右旋轉的程式碼就行啦
注意綠色的條件要搞清楚
3. 刪除節點
原理和新增節點一樣,直接看程式碼吧:
// 從二分搜尋樹中刪除鍵為key的節點
public V remove(K key){
Node node = getNode(root, key);
if(node != null){
root = remove(root, key);
return node.value;
}
return null;
}
private Node remove(Node node, K key){
if( node == null )
return null;
Node retNode;
if( key.compareTo(node.key) < 0 ){
node.left = remove(node.left , key);
// return node;
retNode = node;
}
else if(key.compareTo(node.key) > 0 ){
node.right = remove(node.right, key);
// return node;
retNode = node;
}
else{ // key.compareTo(node.key) == 0
// 待刪除節點左子樹為空的情況
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
// return rightNode;
retNode = rightNode;
}
// 待刪除節點右子樹為空的情況
else if(node.right == null){
Node leftNode = node.left;
node.left = null;
size --;
// return leftNode;
retNode = leftNode;
}
// 待刪除節點左右子樹均不為空的情況
else{
// 找到比待刪除節點大的最小節點, 即待刪除節點右子樹的最小節點
// 用這個節點頂替待刪除節點的位置
Node successor = minimum(node.right);
//successor.right = removeMin(node.right);
successor.right = remove(node.right, successor.key);
successor.left = node.left;
node.left = node.right = null;
// return successor;
retNode = successor;
}
}
if(retNode == null)
return null;
// 更新height
retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));
// 計算平衡因子
int balanceFactor = getBalanceFactor(retNode);
// 平衡維護
// LL
if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0)
return rightRotate(retNode);
// RR
if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0)
return leftRotate(retNode);
// LR
if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
retNode.left = leftRotate(retNode.left);
return rightRotate(retNode);
}
// RL
if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0) {
retNode.right = rightRotate(retNode.right);
return leftRotate(retNode);
}
return retNode;
}