數據結構之AVL樹

平衡樹和AVL

咱們先來回憶一下二分搜索樹所存在的一個問題:當咱們按順序往二分搜索樹添加元素時,那麼二分搜索樹可能就會退化成鏈表。例如,如今有這樣一顆二分搜索樹:
數據結構之AVL樹java

接下來咱們依次插入以下五個節點:七、六、五、四、3。按照二分搜索樹的特性,這棵樹就會變成以下這樣:
數據結構之AVL樹node

可見在極端的狀況下,若是往一棵二分搜索樹添加元素時,徹底是按照順序添加的,那麼此時二分搜索樹就會退化成鏈表,$O(logn)$ 時間複雜度退化到 $O(n)$。 數據結構

這是由於二分搜索樹不具備自平衡的特性,爲了讓二分搜索樹不退化成鏈表,咱們就得設計一種機制,即使是在按順序添加元素時,也能讓二分搜索樹維持平衡。而具備自平衡特性的二叉樹或 m 叉樹,就稱之爲平衡樹。ide

而這個「平衡」其實有幾種狀況,有絕對平衡:任意節點的左右子樹高度相等(2-3樹);高度平衡:任意節點的左右子樹高度相差不超過 1(AVL樹);近似平衡:任意節點的左右子樹高度相差不超過 2,或者說從根節點到葉子節點的最長路徑不大於最短路徑的 2 倍(紅黑樹)。學習

基本上只要一棵樹的高度和節點數量之間的關係始終是 $O(logn)$,也就是不會發生退化狀況的,就能稱之爲平衡樹。若是敢說一棵樹是」平衡「的,就意味着它的高度是 logn 級別的。也就意味着對這棵樹的基本操做(增刪改查)是 logn 級別的。字體

其中 AVL 樹是最先被髮明出來的平衡樹,AVL 這個名稱來自於它的兩位發明者 G.M. Adelson-VelskyE.M. Landis 的首字母,AVL 樹在他們1962年的論文中首次提出。因此,能夠認爲 AVL 樹是最先的自平衡二分搜索樹結構。AVL 樹遵循的是高度平衡,任意節點的左右子樹高度相差不超過 1。this


計算節點的高度和平衡因子

通過以上的介紹,如今咱們已經知道了AVL樹是一種平衡的二分搜索樹。那麼爲了維持AVL樹的平衡,咱們就得作一些額外的工做。首先,咱們得知道AVL樹的平衡狀態,能夠經過一些依據判斷AVL樹是否已經失衡了。若是處於失衡狀態,就須要對AVL樹作出一系列的調整使得它維持平衡。設計

判斷AVL樹是否平衡的主要依據是節點的平衡因子,而平衡因子則經過節點的高度計算得出。下圖中,用黑色字體標記的是節點的高度,藍色字體標記的是節點的平衡因子:
數據結構之AVL樹3d

上圖中的二叉樹不是一棵合格的AVL樹,由於只有當一棵二叉樹全部節點的平衡因子都是 -一、0、1這 三個值時,這棵二叉樹才能算是一棵合格的AVL樹。以下圖所示:
數據結構之AVL樹code

  • 其中節點 4 的左子樹高度是 1,右子樹不存在,因此該節點的平衡因子是 $1-0=1$
  • 節點7的左子樹不存在,右子樹高度是1,因此平衡因子是 $0-1=-1$
  • 全部的葉子節點,不存在左右子樹,因此平衡因子都是 0

爲了計算節點的平衡因子,咱們須要在每一個節點中新增長一個字段,存儲節點的高度。而平衡因子的計算也很簡單,用左子節點的高度減去右子節點的高度就能夠了。也就是說,平衡因子就是左右子樹高度的差值。

接下來,咱們先實現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樹

  • 在上圖中,身爲右子節點的Y取代了X的位置,而X變成了本身的左子節點,所以爲左旋轉

右旋轉:順時針旋轉紅黑樹的兩個節點,使得父節點被本身的左子節點取代,而本身成爲本身的右子節點。以下圖:
數據結構之AVL樹

  • 在上圖中,身爲左子節點的Y取代了X的位置,而X變成了本身的右子節點,所以爲右旋轉

那麼AVL樹何時須要進行左旋轉,何時須要進行右旋轉呢?這得看樹的傾斜狀況,由於不一樣的傾斜狀況,須要採起不一樣的旋轉方式。主要分爲四種狀況,對應着四種旋轉方式。這裏將其稱爲:

  • 左左狀況(LL),單次右旋轉
  • 右右狀況(RR),單次左旋轉
  • 左右狀況(LR),先左旋轉,後右旋轉
  • 右左狀況(RL),先右旋轉,後左旋轉

若是你有學習過如何還原魔方的話,就會發現AVL樹的平衡過程跟魔方的還原很是類似。魔方的還原是有固定公式的:根據色塊在一個面上的不一樣排列狀況,都有相應的旋轉步驟。只要跟着這個還原步驟,最終就能將魔方還原。

而AVL樹的平衡大體過程就是:遇到什麼樣的節點排布,咱們就對應怎麼去旋轉調整。只要按照這些固定的旋轉規則來操做,就能將一個非平衡的AVL樹調整成平衡的。這裏不一樣的節點排布就對應着上述所說的四種狀況,接下來咱們就看看這四種狀況及其解法。

一、左左狀況(LL),簡單來講就是總體左傾的狀況,傾斜發生在節點左子樹中的最左子節點。以下圖:
數據結構之AVL樹

  • 圖中的三角形表示各個節點的子樹

在這種狀況下,咱們須要從下往上找到發生傾斜的子樹的根節點,即該子樹中平衡因子大於 1 的那個節點。在此例中就是 y 節點,此時咱們以 y 節點爲軸,進行一次右旋轉,從而矯正這棵樹:
數據結構之AVL樹

二、右右狀況(RR)是總體右傾的狀況,傾斜發生在節點右子樹中的最右子節點。以下圖:
數據結構之AVL樹

在這種狀況下,一樣從下往上找到相應的根節點,而後以根節點 y 爲軸,進行一次左旋轉:
數據結構之AVL樹

三、左右狀況(LR),傾斜發生在節點左子樹中的最右子節點。以下圖:
數據結構之AVL樹

在這種狀況下,咱們就須要分兩步走了,先以 x 節點爲軸,進行左旋轉:
數據結構之AVL樹

能夠看到此時就轉換成了左左狀況(LL),那麼就只須要按照左左狀況的方式,以 y 節點爲軸,進行右旋轉便可:
數據結構之AVL樹

四、右左狀況(RL),傾斜發生在節點右子樹中的最左子節點。以下圖:
數據結構之AVL樹

一樣,在這種狀況下,咱們也須要分兩步走,先以 x 節點爲軸,進行右旋轉:
數據結構之AVL樹

轉換成了右右狀況(RR)後,按照這種狀況的方式,以 y 節點爲軸,進行左旋轉:
數據結構之AVL樹

以上就是AVL樹須要調整平衡的四種狀況,以及四種對應的調整方式。如今讓咱們來看本小節最開始的那個例子,在該例子中,以節點4 爲根的左子樹出現了不平衡的狀況。如今來看,該子樹正好符合 「左左狀況」。因而,咱們以節點 4 爲軸,進行右旋操做,就讓AVL樹從新恢復了高度平衡:
數據結構之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樹中添加元素的功能。具體代碼以下:

/**
 * 向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樹的刪除操做理解起來就比較容易了。無非就是在二分搜索樹的刪除操做的基礎上增長了維護平衡的操做,而這個操做與添加元素時是徹底同樣的。

咱們來看個例子:
數據結構之AVL樹

如上圖所示,咱們在AVL樹中刪除了節點 1,致使父節點 2 的平衡因子變爲了 -2,打破了AVL樹的平衡。此時,以節點 2 爲根的子樹正好造成了「右左狀況(RL)」,因而咱們首先以節點 4 爲軸進行右旋轉:
數據結構之AVL樹

而後再以節點 2 爲軸進行左旋轉:
數據結構之AVL樹

通過如上步驟後,最終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;
}
相關文章
相關標籤/搜索