AVL 平衡二叉樹
文章目錄
1、爲什麼要有AVL平衡二叉樹
我們之前寫過文章叫做BST二分搜索樹,這種數據結構有一個缺點就是會退化爲鏈表形式,到這導致了我們的樹結構發揮不出來它應有的優勢。
從上圖可以發現如果按照順序進行添加操作,那麼二分搜索樹就會退化爲鏈表形式,樹結構也就失去了它的意義。
AVL(Adelson-Velsky-Landis Tree)是以創造者的名字命名的。這種樹結構就是一種平衡二叉樹。它是最早的認爲可以自平衡的一種樹結構。
2、什麼是AVL平衡二叉樹
我們之前寫過的滿二叉樹、完全二叉樹、線段樹、最大堆等等都是一種平衡樹的例子(葉子節點的高度差不會大於 1 )。其實上面的平衡二叉樹都是比較理想的例子。但是在AVL樹中維護的平衡二叉樹有所不同。
對於任意一個節點,左子樹和右子樹的高度差不能超過 1
上面的規則相對來說更加寬鬆一些。比如下面這張圖:
這個結構並不滿足我們之前的平衡二叉樹的規則,根節點的左右子樹高度差不大於 1 。卻滿足我們上面的規則(對於任意節點)。
對於時間複雜度方面,平衡二叉樹的高度和節點數量之間也是O(log (N) )。
3、AVL樹的基本實現
3.1、實現的方法
首先,我們需要標註每個節點的高度信息和平衡因子,平衡因子的計算就是左子樹的高度減去右子樹的高度的絕對值。這樣我們就可以依靠平衡因子來維護的AVL樹結構。
3.2、構造函數
我們首先需要構造一個節點信息作爲內部類,主要包含我們要存儲的內容,左右子樹的指針,還有高度信息。對於平衡因子我們可以使用左右子樹的差來進行計算。
節點信息:
private class Node{ //內部類
public K key;
public V value;
public Node left, right;
public int height;
public Node(K key, V value){
this.key = key;
this.value = value;
left = null;
right = null;
height = 1; //新節點高度爲 1
}
構造函數:
private Node root;
private int size;
public AVLTree(){
root = null;
size = 0;
}
3.3、基本成員函數
首先我們需要獲取樹結構的大小信息getSize()方法和判斷是否爲空isEmpty()方法。
程序實現:
public int getSize() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
因爲我們需要高度信息來判斷樹結構,所以引入getHeight()方法。
程序實現:
private int getHeight(Node node) {
if (node == null)
return 0;
return node.height;
}
爲了方便我們後續的操作,我們引入getNode()方法,通過索引key值來獲得節點。
程序實現:
private Node getNode(K key) {
return getNode(root, key);
}
private Node getNode(Node node, K key) {
if (node == null)
return null;
if (node.key.compareTo(key) < 0)
return getNode(node.left, key);
else if (node.key.compareTo(key) > 0)
return getNode(node.right, key);
else
return node;
}
在添加操作的時候,我們需要根據高度信息來獲得平衡因子,進而判斷樹結構是否滿足平衡樹的性質。
大於0:偏左,小於0:偏右
程序實現:
private int getBalanceFactor(Node node){
if (node == null)
return 0;
return getHeight(node.left) - getHeight(node.right);
}
4、左旋轉和右旋轉
我們知道原來平衡的樹變成不平衡會是在添加元素的時候,所以在添加元素的時候我們需要維護平衡性。下面就分四種情況分別討論:
4.1、LL 右旋轉
有一種添加元素後的情況是這樣的。
可以認爲添加的元素在節點的左邊(L)的右邊(L)。
在添加元素2,後會導致樹結構的平衡性破壞,對於這種情況的判斷就是當前節點的平衡因子大於1(向左偏斜),並且左節點的平衡因子大於0(向左偏斜),這樣就保證了這種情況的出現,向左偏斜。
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)
那麼我們就需要對上面的結構進行維護。進行右旋轉操作。
爲了不失一般性,我們引入更復雜的情況,如下圖:
按照平衡二叉樹的排序規則,對元素進行右旋轉(類似於 y 繞 x 右旋轉),相當於降低樹的高度。旋轉後仍然滿足二叉樹的排序規則。我們可以發現相對位置發生改變的就是節點 y 和 x 的右子樹 T3 。最後更新高度信息,高度發生變化的只有節點 x , y 。
程序實現:
private Node rightRotate(Node y) {
Node x = y.left; //獲得旋轉中心
Node T3 = x.right;
x.right = y; //進行旋轉操作
y.left = T3;
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1; //重新更新高度信息
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
4.2、RR 左旋轉
我們有了上面的右旋轉的概念,那麼左旋轉就變得簡單多了,左旋轉的發生的前提就是節點的平衡因子小於-1(偏右),右子樹的平衡因子小於0(偏右),也就是類似於下面的結構。
可以認爲添加的元素在節點的右邊(R)的右邊(R)。
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)
下面就是對結構進行左旋轉操作,維護平衡性。
以x爲中心,進行左旋轉操作(類似於 y 繞 x 左旋轉),需要轉移的分別是 x 的左子樹 T2 和節點 y 。
程序實現:
private Node leftRotate(Node y) {
Node x = y.right;
Node T2 = x.left;
x.left = y; //需要轉移的元素
y.right = T2;
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1; // 更新高度信息
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
4.3、LR 左右旋轉
這種情況就是添加元素在節點的左邊的右邊,左右旋轉指的是先進行左旋轉,後進行右旋轉。類似於下面這種情況。
具體細節的旋轉如下:
先進行左旋轉操作,結構就會變成我們之前LL的形式,然後對其進行右旋轉操作即可。
4.4、RL 右左旋轉
這種情況就是添加元素在節點的右邊的左邊,右左旋轉指的是先進行右旋轉,後進行左旋轉。類似於下面這種情況。
具體細節的旋轉如下:
先進行左旋轉操作,結構就會變成我們之前LL的形式,然後對其進行右旋轉操作即可。
4.5、四種情況總結
上面的四種情況完全包含了添加元素所需要的情況。
// LL
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)
return rightRotate(node);
// RR
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)
return leftRotate(node);
// LR
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0){
node.left = leftRotate(node.left);
return rightRotate(node);
}
// RL
if (balanceFactor < -1 && getBalanceFactor(node.right) > 0){
node.right = rightRotate(node.right);
return leftRotate(node);
}
5、增刪改查操作的實現
5.1、添加操縱
步驟:
- 遞歸到底的時候添加元素,否則就更新元素
- 更新每個節點的高度信息
- 對結構進行平衡處理
private Node add(Node node, K key, V value) {
/*
* BST 的源代碼片段 ····
*/
node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
int balanceFactor = getBalanceFactor(node);
// LL
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)
return rightRotate(node);
// RR
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)
return leftRotate(node);
// LR
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0){
node.left = leftRotate(node.left);
return rightRotate(node);
}
// RL
if (balanceFactor < -1 && getBalanceFactor(node.right) > 0){
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
5.2、刪除操作
這裏的刪除操作,在原本BST二分搜索樹的基礎上進行改變,增加刪除元素後對節點進行平衡後的處理,同添加操作基本相同,這裏只對增加的程序片段進行展示。
private Node remove(Node node, K key) {
/*
* BST 的源代碼片段 ····
*/
if (retNode == null)
return null;
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;
}
5.3、查詢操作
查詢操作主要包含查看元素是否在樹結構中,另一個就是通過key值查找對應的 value 值。兩者均是藉助getNode方法進行實現。
程序實現:
public boolean contains(K key) {
return getNode(key) != null;
}
public V get(K key) {
Node node = getNode(key);
return node == null ? null : node.value;
}
5.4、更改操作
更改操作也是藉助getNode方法進行更改。
public void set(K key, V newValue) {
Node node = getNode(key);
if (node != null)
node.value = newValue;
else
throw new IllegalArgumentException(key + "doesn't exist");
}
最後
更多精彩內容,大家可以轉到我的主頁:曲怪曲怪的主頁
或者關注我的微信公衆號:TeaUrn
源碼地址:可在公衆號內回覆 數據結構與算法源碼 即可獲得。