咱們先來回憶一下二分搜索樹所存在的一個問題:當咱們按順序往二分搜索樹添加元素時,那麼二分搜索樹可能就會退化成鏈表。例如,如今有這樣一顆二分搜索樹:java
接下來咱們依次插入以下五個節點:七、六、五、四、3。按照二分搜索樹的特性,這棵樹就會變成以下這樣:node
可見在極端的狀況下,若是往一棵二分搜索樹添加元素時,徹底是按照順序添加的,那麼此時二分搜索樹就會退化成鏈表,$O(logn)$ 時間複雜度退化到 $O(n)$。 數據結構
這是由於二分搜索樹不具備自平衡的特性,爲了讓二分搜索樹不退化成鏈表,咱們就得設計一種機制,即使是在按順序添加元素時,也能讓二分搜索樹維持平衡。而具備自平衡特性的二叉樹或 m 叉樹,就稱之爲平衡樹。ide
而這個「平衡」其實有幾種狀況,有絕對平衡:任意節點的左右子樹高度相等(2-3樹);高度平衡:任意節點的左右子樹高度相差不超過 1(AVL樹);近似平衡:任意節點的左右子樹高度相差不超過 2,或者說從根節點到葉子節點的最長路徑不大於最短路徑的 2 倍(紅黑樹)。學習
基本上只要一棵樹的高度和節點數量之間的關係始終是 $O(logn)$,也就是不會發生退化狀況的,就能稱之爲平衡樹。若是敢說一棵樹是」平衡「的,就意味着它的高度是 logn 級別的。也就意味着對這棵樹的基本操做(增刪改查)是 logn 級別的。字體
其中 AVL 樹是最先被髮明出來的平衡樹,AVL 這個名稱來自於它的兩位發明者 G.M. Adelson-Velsky 和 E.M. Landis 的首字母,AVL 樹在他們1962年的論文中首次提出。因此,能夠認爲 AVL 樹是最先的自平衡二分搜索樹結構。AVL 樹遵循的是高度平衡,任意節點的左右子樹高度相差不超過 1。this
通過以上的介紹,如今咱們已經知道了AVL樹是一種平衡的二分搜索樹。那麼爲了維持AVL樹的平衡,咱們就得作一些額外的工做。首先,咱們得知道AVL樹的平衡狀態,能夠經過一些依據判斷AVL樹是否已經失衡了。若是處於失衡狀態,就須要對AVL樹作出一系列的調整使得它維持平衡。設計
判斷AVL樹是否平衡的主要依據是節點的平衡因子,而平衡因子則經過節點的高度計算得出。下圖中,用黑色字體標記的是節點的高度,藍色字體標記的是節點的平衡因子:3d
上圖中的二叉樹不是一棵合格的AVL樹,由於只有當一棵二叉樹全部節點的平衡因子都是 -一、0、1這 三個值時,這棵二叉樹才能算是一棵合格的AVL樹。以下圖所示:code
爲了計算節點的平衡因子,咱們須要在每一個節點中新增長一個字段,存儲節點的高度。而平衡因子的計算也很簡單,用左子節點的高度減去右子節點的高度就能夠了。也就是說,平衡因子就是左右子樹高度的差值。
接下來,咱們先實現AVL樹的基礎代碼:
package tree.avl; import java.util.ArrayList; /** * AVL樹 * * @author 01 * @date 2021-01-29 **/ public class AVLTree<K extends Comparable<K>, V> { 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; } } private Node root; private int size; public AVLTree() { root = null; size = 0; } public int getSize() { return size; } public boolean isEmpty() { return size == 0; } /** * 得到節點node的高度 */ private int getHeight(Node node) { return node == null ? 0 : node.height; } /** * 得到節點node的平衡因子 */ private int getBalanceFactor(Node node) { if (node == null) { return 0; } return getHeight(node.left) - getHeight(node.right); } }
有了判斷平衡狀態的依據後,咱們就能夠判斷AVL樹的平衡性了。除此以外,因爲AVL樹本質上是一棵平衡版的二分搜索樹,因此咱們還須要檢查AVL樹的二分搜索樹性質。由於,調整AVL樹的過程當中可能會破壞二分搜索樹的性質,此時就須要將其「矯正」過來。
判斷AVL樹的平衡性很簡單,就是看各個節點的平衡因子是否大於1便可。由於平衡因子本質上只是左右子樹高度的差值,而AVL樹的定義是這個差值不能大於1。檢查二分搜索樹的性質也不難,經過中序遍歷就能夠作到。由於一棵樹知足二分搜索樹的性質,那麼中序遍歷必然是有序的,若是獲得的結果是無序的就證實不知足二分搜索樹的性質。
具體的實現代碼以下:
/** * 檢查當前的AVL樹是否知足二分搜索樹的性質 */ public boolean isBST() { ArrayList<K> keys = new ArrayList<>(); inOrder(root, keys); for (int i = 1; i < keys.size(); i++) { // 中序遍歷一棵二分搜索樹所獲得的key理應是有序的 // 若是是無序的,就證實不知足二分搜索樹的性質 if (keys.get(i - 1).compareTo(keys.get(i)) > 0) { return false; } } return true; } /** * 中序遍歷以node爲根的二叉樹,並將每一個節點的key放到keys中 */ private void inOrder(Node node, ArrayList<K> keys) { if (node == null) { return; } inOrder(node.left, keys); keys.add(node.key); inOrder(node.right, keys); } /** * 檢查當前AVL樹的平衡性 */ public boolean isBalanced() { return isBalanced(root); } /** * 判斷以Node爲根的二叉樹是不是一棵平衡二叉樹,遞歸實現 */ private boolean isBalanced(Node node) { if (node == null) { return true; } int balanceFactor = getBalanceFactor(node); // AVL對平衡的定義是:左右子樹高度相差不能大於1 if (Math.abs(balanceFactor) > 1) { return false; } return isBalanced(node.left) && isBalanced(node.right); }
通過前面的鋪墊,如今咱們已經完成了AVL樹維持平衡時所需的輔助功能。接下來,咱們看看AVL樹是怎麼維持平衡的。首先,咱們得知道AVL樹何時會發平生衡性被打破的狀況。
與其餘樹形結構同樣,當AVL樹添加或刪除節點時,其平衡性就有可能會被打破。以下圖所示:
那麼AVL樹是怎麼維持平衡的呢?以前在紅黑樹的文章中提到過,紅黑樹是經過變色、左旋及右旋轉這三種操做來維持平衡的。
由於AVL樹中的節點沒有顏色的概念,因此不存在變色的問題,只有左旋轉、右旋轉這兩種維持平衡的操做。而且AVL樹中的左旋轉和右旋轉,和以前紅黑樹中所介紹的是同樣的。
左旋轉:逆時針旋轉紅黑樹的兩個節點,使得父節點被本身的右子節點取代,而本身成爲本身的左子節點。以下圖:
右旋轉:順時針旋轉紅黑樹的兩個節點,使得父節點被本身的左子節點取代,而本身成爲本身的右子節點。以下圖:
那麼AVL樹何時須要進行左旋轉,何時須要進行右旋轉呢?這得看樹的傾斜狀況,由於不一樣的傾斜狀況,須要採起不一樣的旋轉方式。主要分爲四種狀況,對應着四種旋轉方式。這裏將其稱爲:
若是你有學習過如何還原魔方的話,就會發現AVL樹的平衡過程跟魔方的還原很是類似。魔方的還原是有固定公式的:根據色塊在一個面上的不一樣排列狀況,都有相應的旋轉步驟。只要跟着這個還原步驟,最終就能將魔方還原。
而AVL樹的平衡大體過程就是:遇到什麼樣的節點排布,咱們就對應怎麼去旋轉調整。只要按照這些固定的旋轉規則來操做,就能將一個非平衡的AVL樹調整成平衡的。這裏不一樣的節點排布就對應着上述所說的四種狀況,接下來咱們就看看這四種狀況及其解法。
一、左左狀況(LL),簡單來講就是總體左傾的狀況,傾斜發生在節點左子樹中的最左子節點。以下圖:
在這種狀況下,咱們須要從下往上找到發生傾斜的子樹的根節點,即該子樹中平衡因子大於 1 的那個節點。在此例中就是 y 節點,此時咱們以 y 節點爲軸,進行一次右旋轉,從而矯正這棵樹:
二、右右狀況(RR)是總體右傾的狀況,傾斜發生在節點右子樹中的最右子節點。以下圖:
在這種狀況下,一樣從下往上找到相應的根節點,而後以根節點 y 爲軸,進行一次左旋轉:
三、左右狀況(LR),傾斜發生在節點左子樹中的最右子節點。以下圖:
在這種狀況下,咱們就須要分兩步走了,先以 x 節點爲軸,進行左旋轉:
能夠看到此時就轉換成了左左狀況(LL),那麼就只須要按照左左狀況的方式,以 y 節點爲軸,進行右旋轉便可:
四、右左狀況(RL),傾斜發生在節點右子樹中的最左子節點。以下圖:
一樣,在這種狀況下,咱們也須要分兩步走,先以 x 節點爲軸,進行右旋轉:
轉換成了右右狀況(RR)後,按照這種狀況的方式,以 y 節點爲軸,進行左旋轉:
以上就是AVL樹須要調整平衡的四種狀況,以及四種對應的調整方式。如今讓咱們來看本小節最開始的那個例子,在該例子中,以節點4 爲根的左子樹出現了不平衡的狀況。如今來看,該子樹正好符合 「左左狀況」。因而,咱們以節點 4 爲軸,進行右旋操做,就讓AVL樹從新恢復了高度平衡:
在上一小節中,咱們介紹了AVL樹爲了維持平衡所使用的旋轉操做,以及不一樣狀況所對應的不一樣旋轉方式。在本小節中,就讓咱們用代碼來實現AVL樹的左旋轉和右旋轉操做。代碼以下:
// 對節點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; }
到目前爲止,咱們就已經瞭解了AVL樹中維持平衡所需的內容。在理論和代碼上咱們都學習到了如何維持一棵AVL樹的平衡性,也已經實現了相應的輔助功能。
那麼也就知道在添加和刪除元素時,如何解決可能破壞AVL樹平衡性的問題。因此,接下來咱們就實現向AVL樹中添加元素的功能。具體代碼以下:
/** * 向AVL樹中添加新的元素(key, value) */ public void add(K key, V value) { root = add(root, key, value); } /** * 向以node爲根的AVL中插入元素(key, value),遞歸實現 * 返回插入新節點後AVL的根 */ 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 { node.value = value; } // 更新height node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right)); // 計算平衡因子 int balanceFactor = getBalanceFactor(node); // --- 維護平衡 start --- // 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); } // --- 維護平衡 end --- return node; }
從AVL樹中刪除元素也會打破AVL樹的平衡性,那麼在刪除元素時如何維持AVL樹的平衡呢?若是在刪除元素時,打破了AVL樹的平衡,其維持平衡的調整方式與以前提到的同樣,仍是根據那四種狀況進行四種旋轉操做便可。
所以,有了前面的基礎,而且對二分搜索樹的刪除操做有必定的瞭解的話,那麼對AVL樹的刪除操做理解起來就比較容易了。無非就是在二分搜索樹的刪除操做的基礎上增長了維護平衡的操做,而這個操做與添加元素時是徹底同樣的。
咱們來看個例子:
如上圖所示,咱們在AVL樹中刪除了節點 1,致使父節點 2 的平衡因子變爲了 -2,打破了AVL樹的平衡。此時,以節點 2 爲根的子樹正好造成了「右左狀況(RL)」,因而咱們首先以節點 4 爲軸進行右旋轉:
而後再以節點 2 爲軸進行左旋轉:
通過如上步驟後,最終AVL樹從新恢復了高度平衡。
AVL樹刪除操做的具體實現代碼以下:
/** * 返回以node爲根的AVL的最小值所在的節點 */ private Node minimum(Node node) { if (node.left == null) { return node; } return minimum(node.left); } /** * 從AVL中刪除鍵爲key的節點 */ public V remove(K key) { Node node = getNode(root, key); if (node != null) { root = remove(root, key); return node.value; } return null; } /** * 刪除以node爲根的AVL中鍵爲key的節點,遞歸實現 * 返回刪除節點後新的AVL的根 */ 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); retNode = node; } else if (key.compareTo(node.key) > 0) { // 待刪除節點在右子樹中 node.right = remove(node.right, key); retNode = node; } else { // 待刪除節點左子樹爲空的狀況 if (node.left == null) { Node rightNode = node.right; node.right = null; size--; 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 = remove(node.right, successor.key); successor.left = node.left; node.left = node.right = null; retNode = successor; } } if (retNode == null) { return null; } // 更新height retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right)); // 計算平衡因子 int balanceFactor = getBalanceFactor(retNode); // --- 維護平衡 start --- // 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); } // --- 維護平衡 end --- return retNode; }