AVL樹的Java實現

定義

Wikipedia - AVL樹node

在計算機科學中,AVL樹是最先被髮明的自平衡二叉查找樹。在AVL樹中,任一節點對應的兩棵子樹的最大高度差爲1,所以它也被稱爲高度平衡樹。查找、插入和刪除在平均和最壞狀況下的時間複雜度都是 {displaystyle O(log {n})} O(log{n})。增長和刪除元素的操做則可能須要藉由一次或屢次樹旋轉,以實現樹的從新平衡。AVL樹得名於它的發明者G. M. Adelson-Velsky和Evgenii Landis,他們在1962年的論文《An algorithm for the organization of information》中公開了這一數據結構。

理論

實現AVL樹的要點爲:每次新增/刪除節點後判斷平衡性而後經過調整使整棵樹從新平衡算法

判斷平衡性:每次新增/刪除節點後,刷新受到影響的節點的高度,便可經過任一節點的左右子樹高度差判斷其平衡性數據結構

調整:經過對部分節點的父子關係的改變使樹從新平衡學習


實現

基本結構

public class Tree<T extends Comparable<T>> {

    private static final int MAX_HEIGHT_DIFFERENCE = 1;

    private Node<T> root;

    class Node<KT> {

        KT key;

        Node<KT> left;

        Node<KT> right;

        int height = 1;

        public Node(KT key, Node<KT> left, Node<KT> right) {
            this.key = key;
            this.left = left;
            this.right = right;
        }
    }
}

插入(insert)

四種不平衡範型

對於任意一次插入所形成的不平衡,均可以簡化爲下述四種範型之一:this

下面四張圖中的數字僅表明節點序號,爲了後文方便展現調整過程
四、五、六、7號節點表明了四棵高度可使不平衡成立的子樹(遵循插入的規則)spa

  • LL型

clipboard.png

  • LR型

clipboard.png

  • RR型

clipboard.png

  • RL型

clipboard.png

總結獲得判斷範型的方法爲:不平衡的節點(節點1)通往高度最大的子樹的葉子節點時所途經的前兩個節點(節點二、節點3)的方向設計

調整方法

  • LL型

clipboard.png

  1. 5號節點做爲1號節點的左孩子
  2. 1號節點做爲2號節點的右孩子

例子(例子中的數字表明節點的值):3d

clipboard.png

插入節點5後形成節點9不平衡,其範型爲LL型,按照固定步驟調整後全局從新達到平衡code

  • LR型

clipboard.png

  1. 6號節點做爲2號節點的右孩子
  2. 7號節點做爲1號節點的左孩子
  3. 2號節點做爲3號節點的左孩子
  4. 1號節點做爲3號節點的右孩子

例子(例子中的數字表明節點的值):orm

clipboard.png

插入節點8.5後形成節點9不平衡,其範型爲LR型,按照固定步驟調整後全局從新達到平衡

  • RR型

clipboard.png

  1. 5號節點做爲1號節點的右孩子
  2. 1號節點做爲2號節點的左孩子

例子(例子中的數字表明節點的值):

clipboard.png

插入節點10.5後形成節點7不平衡,其範型爲RR型,按照固定步驟調整後全局從新達到平衡

  • RL型

clipboard.png

  1. 7號節點做爲2號節點的左孩子
  2. 6號節點做爲1號節點的右孩子
  3. 2號節點做爲3號節點的右孩子
  4. 1號節點做爲3號節點的左孩子

例子(例子中的數字表明節點的值):

clipboard.png

插入節點7.5後形成節點7不平衡,其範型爲RL型,按照固定步驟調整後全局從新達到平衡

代碼實現

public void insert(T key) {
    if (key == null) {
        throw new NullPointerException();
    }
    root = insert(root, key);
}

private Node<T> insert(Node<T> node, T key) {
    if (node == null) {
        return new Node<>(key, null, null);
    }

    int cmp = key.compareTo(node.key);
    if (cmp == 0) {
        return node;
    }
    if (cmp < 0) {
        node.left = insert(node.left, key);
    } else {
        node.right = insert(node.right, key);
    }

    if (Math.abs(height(node.left) - height(node.right)) > MAX_HEIGHT_DIFFERENCE) {
        node = balance(node);
    }
    refreshHeight(node);
    return node;
}

private int height(Node<T> node) {
    if (node == null) {
        return 0;
    }
    return node.height;
}

private void refreshHeight(Node<T> node) {
    node.height = Math.max(height(node.left), height(node.right)) + 1;
}

/**
 * 此方法中的node, node1, node2分別表明上文範型中的一、二、3號節點
 */
private Node<T> balance(Node<T> node) {
    Node<T> node1, node2;
    // ll
    if (height(node.left) > height(node.right) &&
            height(node.left.left) > height(node.left.right)) {
        node1 = node.left;
        node.left = node1.right;
        node1.right = node;

        refreshHeight(node);
        return node1;
    }
    // lr
    if (height(node.left) > height(node.right) &&
            height(node.left.right) > height(node.left.left)) {
        node1 = node.left;
        node2 = node.left.right;
        node.left = node2.right;
        node1.right = node2.left;
        node2.left = node1;
        node2.right = node;

        refreshHeight(node);
        refreshHeight(node1);
        return node2;
    }
    // rr
    if (height(node.right) > height(node.left) &&
            height(node.right.right) > height(node.right.left)) {
        node1 = node.right;
        node.right = node1.left;
        node1.left = node;

        refreshHeight(node);
        return node1;
    }
    // rl
    if (height(node.right) > height(node.left) &&
            height(node.right.left) > height(node.right.right)) {
        node1 = node.right;
        node2 = node.right.left;
        node.right = node2.left;
        node1.left = node2.right;
        node2.left = node;
        node2.right = node1;

        refreshHeight(node);
        refreshHeight(node1);
        return node2;
    }
    return node;
}

總結

由插入節點致使的局部不平衡均會符合上述四種範型之一,只須要按照固定的方式調整相關節點的父子關係便可使樹恢復平衡

關於調整,不少博客或者書籍中將這種調整父子關係的過程稱爲旋轉,這個就見仁見智了,我的以爲這種描述並不容易理解,故本文統一稱爲調整

刪除(remove)

一般狀況

對於刪除節點這個操做來講,有兩個要點:被刪除節點的空缺應該如何填補以及刪除後如何使樹恢復平衡

  • 被刪除節點的空缺應該如何填補
  1. 若是被刪除節點是葉子節點,則不須要填補空缺
  2. 而若是是枝幹節點,則須要填補空缺,理想的狀況是使用某個節點填補被刪除節點的空缺後,整棵樹仍然保持平衡
    a) 若是節點的左右子樹有一棵爲空,則使用非空子樹填補空缺
    b) 若是節點的左右子樹均爲非空子樹,則使用節點的左右子樹中更高的那棵子樹中的最大/最小節點來填補空缺(若是子樹高度一致則哪邊均可以)

例子:

clipboard.png

  1. 假設待刪除節點爲節點9,則應當使用左子樹中的最大值節點8來填補空缺
  2. 假設待刪除節點爲節點13,則應當使用右子樹中的最小值節點14來填補空缺
  3. 假設待刪除節點爲節點2,則使用左子樹中的最大值節點1.5或者右子樹中的最小值節點2.5來填補空缺都可

按照上述方式來填補空缺,能夠儘量保證刪除後整棵樹仍然保持平衡

  • 刪除後如何使樹恢復平衡

clipboard.png

如圖,葉子節點12爲被刪除節點,刪除後不須要填補空缺,可是此時節點13產生了不平衡

不過節點13的不平衡知足上文所說的不平衡範型中的RR型,所以只須要對節點13作對應的調整便可,如圖:

clipboard.png

此時節點13所在的子樹通過調整從新達到局部平衡

可是咱們緊接着發現,節點11出現了不平衡,其左子樹高度爲4,右子樹高度爲2

若是此時按照插入狀況下的不平衡範型判斷方法去判斷節點11的不平衡狀況屬於哪一種範型,會發現沒法知足四種範型的任一狀況

特殊狀況

由刪除節點致使的不平衡,除了會出現插入中所說的四種範型以外,還會出現兩種狀況,如圖:

clipboard.png

整棵樹初始狀態爲平衡狀態,此時假設刪除節點13節點14,均會致使節點11產生不平衡(左子樹高度3,右子樹高度1)

可是若是仍然按照插入時的方法來判斷不平衡,則會發現,節點4的左右子樹高度一致,即在知足了L後,後續沒法判斷這種狀況屬於哪一種範型

對於R方向也是同樣

本文稱它們爲L型R型

不過這兩種狀況的處理也很簡單,實際上當出現這種狀況時,使用LL型LR型的調整方法都可以達到使樹從新平衡的目的

如圖:

clipboard.png

兩種調整方式都可使樹從新平衡,對於R型也是同樣,這裏再也不贅述

代碼實現

public void remove(T key) {
    if (key == null) {
        throw new NullPointerException();
    }
    root = remove(root, key);
}

private Node<T> remove(Node<T> node, T key) {
    if (node == null) {
        return null;
    }

    int cmp = key.compareTo(node.key);
    if (cmp < 0) {
        node.left = remove(node.left, key);
    }
    if (cmp > 0){
        node.right = remove(node.right, key);
    }
    if (cmp == 0) {
        if (node.left == null || node.right == null) {
            return node.left == null ? node.right : node.left;
        }
        var successorKey = successorOf(node).key;
        node = remove(node, successorKey);
        node.key = successorKey;
    }

    if (Math.abs(height(node.left) - height(node.right)) > MAX_HEIGHT_DIFFERENCE) {
        node = balance(node);
    }
    refreshHeight(node);
    return node;
}

/**
 * 尋找被刪除節點的繼承者
 */
private Node<T> successorOf(Node<T> node) {
    if (node == null) {
        throw new NullPointerException();
    }
    if (node.left == null || node.right == null) {
        return node.left == null ? node.right : node.left;
    }

    return height(node.left) > height(node.right) ?
            findMax(node.left, node.left.right, node.left.right == null) :
            findMin(node.right, node.right.left, node.right.left == null);
}

private Node<T> findMax(Node<T> node, Node<T> right, boolean rightIsNull) {
    if (rightIsNull) {
        return node;
    }
    return findMax((node = right), node.right, node.right == null);
}

private Node<T> findMin(Node<T> node, Node<T> left, boolean leftIsNull) {
    if (leftIsNull) {
        return node;
    }
    return findMin((node = left), node.left, node.left == null);
}

其中用到的private Node<T> balance(Node<T> node)方法修改成:

private Node<T> balance(Node<T> node) {
    Node<T> node1, node2;
    // ll & l
    if (height(node.left) > height(node.right) &&
            height(node.left.left) >= height(node.left.right)) {
        node1 = node.left;
        node.left = node1.right;
        node1.right = node;

        refreshHeight(node);
        return node1;
    }
    // lr
    if (height(node.left) > height(node.right) &&
            height(node.left.right) > height(node.left.left)) {
        node1 = node.left;
        node2 = node.left.right;
        node.left = node2.right;
        node1.right = node2.left;
        node2.left = node1;
        node2.right = node;

        refreshHeight(node);
        refreshHeight(node1);
        return node2;
    }
    // rr & r
    if (height(node.right) > height(node.left) &&
            height(node.right.right) >= height(node.right.left)) {
        node1 = node.right;
        node.right = node1.left;
        node1.left = node;

        refreshHeight(node);
        return node1;
    }
    // rl
    if (height(node.right) > height(node.left) &&
            height(node.right.left) > height(node.right.right)) {
        node1 = node.right;
        node2 = node.right.left;
        node.right = node2.left;
        node1.left = node2.right;
        node2.left = node;
        node2.right = node1;

        refreshHeight(node);
        refreshHeight(node1);
        return node2;
    }
    return node;
}

也就是將L型狀況包含進了LL型R型的狀況包含進了RR型,由於這兩種範式的調整要比對應的LR型/RL型的操做數少

總結

儘管刪除節點時會出現特殊的狀況,可是仍然能夠經過簡單的調整使樹始終保持平衡

完整代碼

/**
 * AVL-Tree
 *
 * @author Shinobu
 * @since 2019/5/7
 */
public class Tree<T extends Comparable<T>> {

    private static final int MAX_HEIGHT_DIFFERENCE = 1;

    private Node<T> root;

    class Node<KT> {

        KT key;

        Node<KT> left;

        Node<KT> right;

        int height = 1;

        public Node(KT key, Node<KT> left, Node<KT> right) {
            this.key = key;
            this.left = left;
            this.right = right;
        }
    }

    public Tree(T... keys) {
        if (keys == null || keys.length < 1) {
            throw new NullPointerException();
        }

        root = new Node<>(keys[0], null, null);
        for (int i = 1; i < keys.length && keys[i] != null; i++) {
            root = insert(root, keys[i]);
        }
    }

    public T find(T key) {
        if (key == null || root == null) {
            return null;
        }
        return find(root, key, key.compareTo(root.key));
    }

    private T find(Node<T> node, T key, int cmp) {
        if (node == null) {
            return null;
        }

        if (cmp == 0) {
            return node.key;
        }

        return find(
                (node = cmp > 0 ? node.right : node.left),
                key,
                node == null ? 0 : key.compareTo(node.key));
    }

    public void insert(T key) {
        if (key == null) {
            throw new NullPointerException();
        }
        root = insert(root, key);
    }

    private Node<T> insert(Node<T> node, T key) {
        if (node == null) {
            return new Node<>(key, null, null);
        }

        int cmp = key.compareTo(node.key);
        if (cmp == 0) {
            return node;
        }
        if (cmp < 0) {
            node.left = insert(node.left, key);
        } else {
            node.right = insert(node.right, key);
        }

        if (Math.abs(height(node.left) - height(node.right)) > MAX_HEIGHT_DIFFERENCE) {
            node = balance(node);
        }
        refreshHeight(node);
        return node;
    }

    private int height(Node<T> node) {
        if (node == null) {
            return 0;
        }
        return node.height;
    }

    private void refreshHeight(Node<T> node) {
        node.height = Math.max(height(node.left), height(node.right)) + 1;
    }

    private Node<T> balance(Node<T> node) {
        Node<T> node1, node2;
        // ll & l
        if (height(node.left) > height(node.right) &&
                height(node.left.left) >= height(node.left.right)) {
            node1 = node.left;
            node.left = node1.right;
            node1.right = node;

            refreshHeight(node);
            return node1;
        }
        // lr
        if (height(node.left) > height(node.right) &&
                height(node.left.right) > height(node.left.left)) {
            node1 = node.left;
            node2 = node.left.right;
            node.left = node2.right;
            node1.right = node2.left;
            node2.left = node1;
            node2.right = node;

            refreshHeight(node);
            refreshHeight(node1);
            return node2;
        }
        // rr & r
        if (height(node.right) > height(node.left) &&
                height(node.right.right) >= height(node.right.left)) {
            node1 = node.right;
            node.right = node1.left;
            node1.left = node;

            refreshHeight(node);
            return node1;
        }
        // rl
        if (height(node.right) > height(node.left) &&
                height(node.right.left) > height(node.right.right)) {
            node1 = node.right;
            node2 = node.right.left;
            node.right = node2.left;
            node1.left = node2.right;
            node2.left = node;
            node2.right = node1;

            refreshHeight(node);
            refreshHeight(node1);
            return node2;
        }
        return node;
    }

    public void remove(T key) {
        if (key == null) {
            throw new NullPointerException();
        }
        root = remove(root, key);
    }

    private Node<T> remove(Node<T> node, T key) {
        if (node == null) {
            return null;
        }

        int cmp = key.compareTo(node.key);
        if (cmp < 0) {
            node.left = remove(node.left, key);
        }
        if (cmp > 0){
            node.right = remove(node.right, key);
        }
        if (cmp == 0) {
            if (node.left == null || node.right == null) {
                return node.left == null ? node.right : node.left;
            }
            var successorKey = successorOf(node).key;
            node = remove(node, successorKey);
            node.key = successorKey;
        }

        if (Math.abs(height(node.left) - height(node.right)) > MAX_HEIGHT_DIFFERENCE) {
            node = balance(node);
        }
        refreshHeight(node);
        return node;
    }
    
    private Node<T> successorOf(Node<T> node) {
        if (node == null) {
            throw new NullPointerException();
        }
        if (node.left == null || node.right == null) {
            return node.left == null ? node.right : node.left;
        }

        return height(node.left) > height(node.right) ?
                findMax(node.left, node.left.right, node.left.right == null) :
                findMin(node.right, node.right.left, node.right.left == null);
    }

    private Node<T> findMax(Node<T> node, Node<T> right, boolean rightIsNull) {
        if (rightIsNull) {
            return node;
        }
        return findMax((node = right), node.right, node.right == null);
    }

    private Node<T> findMin(Node<T> node, Node<T> left, boolean leftIsNull) {
        if (leftIsNull) {
            return node;
        }
        return findMin((node = left), node.left, node.left == null);
    }

}

結語

AVL樹的實現,在瞭解了不平衡的六種狀況,以及對應的處理方式後,仍是比較簡單且邏輯清晰的

經過對AVL樹的學習,能夠發現它是一種「對不平衡很是敏感」的結構——能夠容忍的高度差僅爲1。這雖然可讓樹儘量的平衡,使查找效率儘量高,但也付出了相應的代價: 調整平衡。

它的插入元素引起的調整的最壞時間複雜度爲O(1),可是刪除引起的最壞時間複雜度爲O(logN),這正是AVL樹的弊端所在。

因此後來的2-3樹、2-3-4樹、紅黑樹都嘗試對這種弊端進行了改進,改進的思路能夠大概理解爲兩種:

  • 使樹徹底平衡
    這是2-3樹和2-3-4樹這兩種結構嘗試的方向。由於形成AVL樹刪除時「雪崩」的緣由正是由於它所能容忍的這一點高度差,在高度差大量積累後,刪除「薄弱」側的節點,就會致使須要大量的調整才能恢復平衡。而若是徹底消除高度差,就能夠避免這種狀況了。
    然而實際的狀況是這兩種樹的實現都算不上簡單,並且反而使插入的調整行爲的時間複雜度變爲了O(logN)。
  • 容忍不平衡
    紅黑樹的思路的核心是增大了可容忍的高度差,從而實現既保證查詢效率(O(logN)),也保證了插入和刪除後調整平衡的效率(O(1))。
    紅黑樹的查詢效率(2 * O(logN))是略低於AVL樹(O(logN))的,可是紅黑樹經過犧牲了少量查詢效率,使插入刪除後的調整效率達到了常數級別。
    紅黑樹算法中的着色策略、對於父節點、叔節點、祖父節點等等節點的顏色判斷、以及相應的調整策略都是通過極度抽象後的結果,所以想要從頭至尾完全理解紅黑樹的設計思想其實仍是有些難度的(理解設計思想並不是照着抽象好的五條規則照本宣科)

以上,但願本文對讀到的朋友能有所幫助

文章若是有謬誤或疏漏,還請務必指正,感謝萬分

相關文章
相關標籤/搜索