AVL 樹 是最早時期發明的自平衡二叉搜索樹之一。是依據它的兩位發明者的名稱命名。
AVL 樹有一個重要的屬性,即平衡因子(Balance Factor),平衡因子 == 某個節點的左右子樹高度差。
AVL 樹特點總結下來有:
- 每個節點的平衡因子有且僅有 1、0、-1,若超過這三個值的範圍,就稱其爲失衡;
- 每個節點左右子樹的高度差不會超過 1;
- 搜索、添加、刪除的時間複雜度爲 O(logn),n 爲 n 個節點。
看上圖,右側圖中二叉樹就可以稱爲AVL 樹。
添加後導致失衡
若再添加一個元素 18,導致它的祖先節點失衡(甚至有可能它的所有祖父節點都失衡)。但是的父節點和非祖先節點不會失衡。
當添加後導致失衡,就要想辦法調整節點,使其再達到平衡狀態。前提是,必須要滿足二叉搜索樹的性質,不能隨意調整。這裏調整方法就是旋轉節點。
旋轉節點
旋轉節點核心調整祖父節點和父節點的位置,使得其調整後的節點達到平衡,在操作的過程中類似旋轉,所以稱爲旋轉節點。
先確定幾個節點的簡稱,方便下面的說明:
- g:祖父節點
- p:父節點
- n:自身節點
LL - 右旋轉(單旋)
LL: 平衡因子小於 -1 的節點。該失衡的節點爲 g,它的左子樹爲 p,p 的左子樹爲 n。因爲 g、p 、n 都在左側,所以稱爲 LL,如下圖,當添加元素 1 之後,造成失衡。
當出現 LL 類型的失衡時,可以通過右旋轉達到平衡,如下圖:
從圖中可以看出,相當於將 g、p、n 三個節點向右側旋轉,達到平衡,即:
g.left = p.right
p.right = g
若節點發生變化,需要注意節點 parent
節點的變化情況,並在更改節點之後,更新 g
和 p
的高度。
RR - 左旋轉(單旋)
RR 的情況和 LL 情況正好相反,所以旋轉也是相反的,旋轉之後的處理和 LL 是一樣的,這個可以在後面的代碼中體現處理,所以不再重複去解釋這種情況。
LR - RR 左旋轉,LL 右旋轉(雙旋)
LR : 比如添加元素 4 後失衡,的失衡節點定爲 g,它的左子樹爲 p,p 的右子樹爲 n。因爲 p 在左,n 在右。所以這種情況被稱爲 LR。
出現 LR 情況需要先對 p 節點左旋轉處理,使其變成 LL 的情況之後,再對 g 做右旋轉,恢復平衡狀態。
注意:圖中標識錯誤,是 p 向左旋轉,g 向右旋轉
每當旋轉後都要更新其對應節點的高度。
RL - LL 右旋轉,RR 左旋轉(雙旋)
RL 的情況與 LR 的情況也是正好相反的。處理上也是需要反着來就好。旋轉後也是一定要更新其對應的節點。
實現代碼
這裏以 RR 爲例,RR 的情況就需要進行左旋轉。
void rotateLeft(Node<E> grand) {
// 創建臨時變量 parent 和 child
Node<E> parent = grand.right;
Node<E> child = parent.left;
// 祖父節點的右子節點爲父節點的左子節點
grand.right = child;
// 父節點的左節點爲祖父節點
parent.left = grand;
/**
旋轉之後,更改節點的指向,方法代碼看下面
*/
}
做完旋轉之後,要更改 g
和 child
節點的 parent
指向。
void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) {
// 調整 grand 的 parent 節點
if (grand.isLeftChild()) {
grand.parent.left = parent;
} else if (grand,isRightChild()) {
grand.parent.right = parent;
} else {
root = parent;
}
if (child != null) {
child.parent = grand;
}
parent.parent = grand.parent;
grand.parent = parent;
/**
調整之後,更新 g 和 p 的高度,方法代碼看下面
*/
}
這裏是在每個 AVL 節點中定義一個 height 屬性,來記錄當前節點的高度,所以可以在 AVL 節點中實現更新高度的方法。
public void updateHeight() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
height = 1 + Math.max(leftHeight, rightHeight);
}
當前節點的高度就是它的左右子節點的高度中的最大值。代碼邏輯如上
這時候,就有可能有個問題,怎麼保證 AVL 節點中的 height 都是一一對應的呢?這裏是在添加方法之後做的處理,核心邏輯就是在每次添加一個節點之後就更新下全部的節點高度就可以,詳細的在下面去深究。
有了上面左旋轉的實現,那麼右旋轉的實現也就能順利得到,就是和左旋轉相反就好。
private void rotateRight(Node<E> grand) {
Node<E> parent = grand.left;
Node<E> child = parent.right;
grand.left = child;
parent.right = grand;
afterRotate(grand, parent, child);
}
知道了左右旋轉和旋轉後更新節點的方法之後,接下來就要來判斷失衡的節點,以及失衡的情況是 LL 還是 RR,或者其他。
首先判斷失衡的節點,判斷一個節點是否平衡,就要看這個節點的平衡因子是否是在 -1 到 1 之間的,即
boolean isBalanced(Node<E> node) {
return Math.abs(((AVLNode<E>)node).balanceFactor()) <= 1;
}
public int balanceFactor() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
return leftHeight - rightHeight;
}
添加節點
當添加一個節點時,因爲當前節點是沒有左右子節點的,所以要判斷的不是當前節點,而是這個節點的父節點祖父節點們是否失衡,這就用到循環遍歷
void afterAdd(Node<E> node) {
while ((node = node.parent) != null) {
if (isBalanced(node)) {
// 更新高度
updateHeight(node);
}
else {
// 恢復平衡
rebalance(node);
break;
}
}
}
看上面代碼中,有一個恢復平衡的方法沒有說過,在調用這個方法時,就已經確定這個節點是失衡狀態,這時要做的就是確定失衡的情況,然後根據不同的情況做相應的旋轉處理。
這裏要先知道節點的最大高度,就是左右子節點高度中的最大值,即
public Node<E> tallerChild() {
int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height;
int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height;
if (leftHeight > rightHeight) return left;
if (leftHeight < rightHeight) return right;
return isLeaf() ? left : right;
}
高度大的節點對應到要處理的節點,可看下面代碼:
void rebalance(Node<E> grand) {
Node<E> parent = ((AVLNode<E>)grand).tallerChild();
Node<E> node = ((AVLNode<E>)parent).tallerChild();
if (parent.isLeftChild()) { // L
if (node.isLeftChild()) { // LL
rotateRight(grand);
}
else { // LR
rotateLeft(parent);
rotateRight(grand);
}
}
else { // R
if (node.isLeftChild()) { // RL
rotateRight(parent);
rotateLeft(grand);
}
else { // RR
rotateLeft(grand);
}
}
}
刪除節點
當刪除節點後,也可能導致 AVL 樹失衡,有可能是父節點或者祖先節點失衡,通過添加節點的處理失衡邏輯後,可以總結刪除節點的失衡處理邏輯,也是先要遍歷查找失衡的節點,然後把這個節點的失衡情況搞出來,最後對應情況做旋轉處理。
所以刪除後的處理就是:
void afterRemove(Node<E> node) {
while ((node = node.parent) != null) {
if (isBalanced(node)) {
// 更新高度
updateHeight(node);
}
else {
// 恢復平衡
rebalance(node);
}
}
}
總結
看代碼,添加或者刪除節點之後都要一一的遍歷節點的父節點和祖先節點,沒有失衡的更新高度,失衡的就恢復平衡,知道 root 節點,爲什麼這樣?
- 添加節點後,可能會導致所有的祖先節點都失衡
- 刪除節點後,可能會導致父節點或祖先接單失衡,恢復平衡後,可能會導致更高層次的祖先節點失衡
所以就需要整體的遍歷到 root 節點。單處理一個節點是不夠的。
時間複雜度上,搜索是 O(logn),添加是 O(logn),只需要 O(1) 次的旋轉,刪除也是 O(logn),最多需要 O(logn) 次的旋轉。