資料結構-平衡二叉樹

小墨魚3發表於2020-02-07

平衡二叉樹(Balanced Binary Tree)

什麼是平衡二叉樹?
平衡二叉樹的基本概念

在介紹平衡二叉樹之前, 我們先來回憶一下二分搜尋樹的一個問題。 假設有一組數[1, 2, 3, 4, 5, 6]如果我們以順序新增到二分搜尋樹中, 那麼這顆二分搜尋樹就會退化成一個連結串列(如圖[1-1]展示)。這就大大降低二分搜尋樹的效率。那麼怎麼解決這個問題呢? 我們需要在現有的二分搜尋樹的基礎上新增一定的機制, 使得我們的二分搜尋樹能夠維持平衡二叉樹。

[圖1-1 [退化成連結串列的二分搜尋樹]]

1-1

那麼平衡二叉樹是什麼? 在我們之前的樹結構中有遇到過平衡二叉樹嗎?

  1. 一顆滿二叉樹一定是一顆平衡二叉樹。

  2. 完全二叉樹(堆)。對於完全二叉樹來說, 空缺的節點部分一定是在樹的右下部分, 相應的對於一顆完全二叉樹整棵樹的葉子節點最大的深度值和最小的深度值相差不會超過1。也就是說我們所有的葉子節點要麼在最後一層, 要麼在倒數第二層。

  3. 線段樹也就是一種平衡二叉樹。雖然線段樹不是一個完全二叉樹, 對於線段樹來說空出來的部分不一定在整個樹的右下角的位置, 但是在一個整體線段樹中葉子節點也是在最後一層或者在倒數第二層。對於整棵樹來說我們葉子節點的深度相差不會超過1。

以上這些都是平衡二叉樹的例子。

平衡二叉樹的定義:

  • 對於任意一個節點, 左子樹和右子樹的高度差不能超過1。

上面的定義看著和我們的之前的完全二叉樹還是線段樹這樣的二叉樹都差不多, 但實際上是有區別的。對於堆和線段樹來說, 可以保證任意一個葉子節點相應的高度差都不超過1。而上面的定義是任意一個節點左右子樹高度差不超過1。在這個定義下我們得到的平衡二叉樹有可能看著不是"那麼的"平衡(如圖[1-2])。

[圖1-2 [一顆平衡二叉樹]]

1-2

該圖中的結構, 顯然是不會出現在堆或者線段樹這兩種樹結構中。這棵樹看起來稍微有一些偏斜, 但如果仔細去驗證每一個節點就會發現, 這棵樹是滿足平衡二叉樹的定義的。

從根節點12開始, 左子樹高度是3, 右子樹高度是2。高度差為1。沒有超過1。
節點8開始, 左子樹高度是2, 右子樹高度1。高度差為1。沒有超過1。
節點18開始, 左子樹高度是1, 右子樹高度0。高度差為1。沒有超過1。
節點5開始, 左子樹高度是1, 右子樹高度0。高度差為1。沒有超過1。

相應的11, 17, 4。這三個節點是葉子節點, 對於葉子節點來說左右子樹都是空, 說明左右子樹高度都為0, 所以差為0, 也沒有超過1。

所以, 這顆樹看起來有些偏斜, 但是, 是在我們這個定義下的一顆平衡二叉樹。
複製程式碼

圖[1-2]已經是一顆平衡二叉樹了, 但是如果我們在這棵樹上新增節點的話, 比如新增一個節點2和節點7, 根據二分搜尋樹的性質, 那麼節點2會從根節點一路找下來, 最終新增到節點4左子樹中, 相應的如果在新增一個節點7, 節點7會新增到節點5右子樹中。就會形成圖[1-3]的樣子。但是已經不在是一顆平衡二叉樹了。

[圖1-3 [一顆失去平衡的二叉樹]]

1-3

可以看到節點8的位置上, 左子樹的高度是3, 右子樹的高度是1。左右子樹的高度差為2。破壞了平衡二叉樹的條件。同理根節點12也是一樣, 他的左子樹高度是4, 右子樹高度是2。高度差為2。所以, 現在這棵二叉樹不在是一顆平衡二叉樹了。

那麼如何保持平衡呢? 我們必須保證在插入節點的時候, 相應的也要顧及這顆樹的右側部分。 因為這棵樹現在看是向左偏斜的。相應的也要填補這棵樹右側空間的節點。才能繼續讓這顆樹維持平衡二叉樹左右子樹高度差不超過1這個性質。

節點高度&平衡因子

在具體開發中, 由於要跟蹤每一個節點對應的高度是多少, 只有這樣才方便我們判斷, 當前的二叉樹是否是平衡的。所以對之前實現的二分搜尋樹來說, 要實現平衡二叉樹我們只要對每一個節點標註節點高度。這個記錄非常的簡單。

標註節點高度過程:

對於葉子節點2高度為1, 4這個節點由於有葉子節點2對應的高度為2, 對於葉子節點7高度為1。

對於5這個節點, 由於有左右兩顆子樹, 左邊的子樹高度為2, 右邊的子樹高度為1, 相應的節點5的高度就是左右兩顆子樹中最高的那棵樹在加上1。這個1是節點5自身。所以節點5的高度是3。

葉子節點11的高度為1, 節點8的高度4, 葉子節點17的高度為1。
節點18的高度為2。根節點的高度為5。

這樣, 我們就對每一個節點都標註好了高度值。
複製程式碼

上面, 我們把節點高度值標註好之後, 相應的我們要計算一個"平衡因子"。
平衡因子: 就是計算左右子樹的高度差。計算方法就是[左子樹高度-右子樹高度]。

平衡因子計算過程:

對於葉子節點2, 它的左右兩顆子樹相當於是兩顆空樹, 空樹的高度記為0, 相應的葉子節點的平衡因子就是(0 - 0)結果為0。

對於節點4來說左子樹高度為1, 右子樹為高度0, 即平衡因子為(1 - 0)為1
葉子節點7的平衡因子也為0

節點5的左子樹高度為2, 右子樹高度為1, 即平衡因子為(2 - 1)為1
葉子節點11的平衡因子也為0

節點8的左子樹高度為3, 右子樹高度為1, 即平衡因子為(3 - 1)為2, 這就意味著對於8這個節點來說, 左右子樹高度差超過1了, 通過這個平衡因子就能看出這棵樹已經不是一顆平衡二叉樹了。換句話說, 只要平衡因此大於1, 這棵樹就不是一顆平衡二叉樹了。

葉子節點17的平衡因子也為0
對於節點18來說左子樹高度為1, 右子樹高度為0, 即平衡因子為(1 - 0)為1

對於根節點12, 左子樹高度為4, 右子樹高度為2, 即平衡因子為(4 - 2)為2
複製程式碼

經過上面平衡因子的計算, 我們很清楚的知道有兩個節點破壞了平衡二叉樹的性質。

[圖1-4 [計算二叉樹的高度和平衡因子]]

1-4

樹高度與平衡因子程式碼實現

我們要實現的平衡樹底層還是利用我們之前學習的二分搜尋樹, 可以沿用之前的程式碼實現, 所以建議在學習完二分搜尋樹之後再來閱讀本篇。當然如果你已經會了二分搜尋樹也沒關係, 我還是會提供一份完全程式碼的清單, 哈哈哈~

這裡我們只需要關注兩個點

  • 節點高度
  • 節點平衡因子

我們在Node類中新增加一個變數"height"來代表當前節點的高度。在構建每一個節點的時候 我們初始化高度都為1, 按照二分搜尋樹新增的特性, 肯定會一路找下去, 最後肯定是一個葉子節點。

我們新增了兩個私有方法, 一個獲取高度, 一個計算平衡因子。

那麼, 我們在什麼時候維護樹的高度以及計算平衡因子呢? 當前, 我們在新增節點的時候, 就會計算當前節點的高度以及平衡因子。



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;

            /**
             *  新增元素的時候, 肯定是一個葉子節點, 所以建立預設的高度就是1
             */
            this.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的高度
     * @param node
     * @return
     */
    private int getHeight(Node node) {
        if (node == null)
            return 0;
        return node.height;
    }

    /***
     * 計算節點node平衡因子
     * @param node
     * @return
     */
    private int getBalanceFactor(Node node) {
        if (node == null)
            return 0;

        /***
         * 平衡因子計算方法:
         *   當前節點左子樹高度 - 當前節點右子樹高度
         */
        return getHeight(node.left) - getHeight(node.right);
    }

    // 向二分搜尋樹中新增新的元素(key, value)
    public void add(K key, V value){
        root = add(root, key, value);
    }

    // 向以node為根的二分搜尋樹中插入元素(key, value),遞迴演算法
    // 返回插入新節點後二分搜尋樹的根
    private Node add(Node node, K key, V value){

        if(node == null){
            size ++;
            return new Node(key, value); // 遍歷到最後一個節點返回, 預設高度為1
        }

        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 // key.compareTo(node.key) == 0
            node.value = value;

        /***
         *  在新增元素的時候, 我們需要維護一下樹的高度。
         *  如何計算高度呢?
         *    當前節點 + Max(左子樹高度, 右子樹高度)
         */
        node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));

        /***
         *  有了高度之後, 我們很輕易的獲取到平衡因子
         */
        int balanceFactor = getBalanceFactor(node);

        // 如果平衡因子大於1, 則破壞了這顆樹的平衡性...
        // 這裡暫時先不處理, 先輸出一段話即可。
        if (Math.abs(balanceFactor) > 1)
            System.out.println("unbalanced : " + balanceFactor);



        return node;
    }

    // 返回以node為根節點的二分搜尋樹中,key所在的節點
    private Node getNode(Node node, K key){

        if(node == null)
            return null;

        if(key.equals(node.key))
            return node;
        else if(key.compareTo(node.key) < 0)
            return getNode(node.left, key);
        else // if(key.compareTo(node.key) > 0)
            return getNode(node.right, key);
    }

    public boolean contains(K key){
        return getNode(root, key) != null;
    }

    public V get(K key){
        Node node = getNode(root, key);
        return node == null ? null : node.value;
    }

    public void set(K key, V newValue){
        Node node = getNode(root, key);
        if(node == null)
            throw new IllegalArgumentException(key + " doesn't exist!");
        node.value = newValue;
    }

    // 返回以node為根的二分搜尋樹的最小值所在的節點
    private Node minimum(Node node){
        if(node.left == null)
            return node;
        return minimum(node.left);
    }

    // 刪除掉以node為根的二分搜尋樹中的最小節點
    // 返回刪除節點後新的二分搜尋樹的根
    private Node removeMin(Node node){

        if(node.left == null){
            Node rightNode = node.right;
            node.right = null;
            size --;
            return rightNode;
        }

        node.left = removeMin(node.left);
        return node;
    }

    // 從二分搜尋樹中刪除鍵為key的節點
    public V remove(K key){

        Node node = getNode(root, key);
        if(node != null){
            root = remove(root, key);
            return node.value;
        }
        return null;
    }

    private Node remove(Node node, K key){

        if( node == null )
            return null;

        if( key.compareTo(node.key) < 0 ){
            node.left = remove(node.left , key);
            return node;
        } else if(key.compareTo(node.key) > 0 ){
            node.right = remove(node.right, key);
            return node;
        } else{   // key.compareTo(node.key) == 0

            // 待刪除節點左子樹為空的情況
            if(node.left == null){
                Node rightNode = node.right;
                node.right = null;
                size --;
                return rightNode;
            }

            // 待刪除節點右子樹為空的情況
            if(node.right == null){
                Node leftNode = node.left;
                node.left = null;
                size --;
                return leftNode;
            }

            // 待刪除節點左右子樹均不為空的情況

            // 找到比待刪除節點大的最小節點, 即待刪除節點右子樹的最小節點
            // 用這個節點頂替待刪除節點的位置
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;

            node.left = node.right = null;

            return successor;
        }
    }
}
複製程式碼
檢查二分搜尋樹性質和平衡性

在介紹AVL樹是如何維持自平衡之前, 我們在做一個輔助工作。輔助方法很簡單

  • 判斷當前樹是否為一顆二分搜尋樹
  • 判斷當前樹是否為平衡二叉樹

對於我們的AVL樹來說, 它是對我們的二分搜尋樹的一個改進。改進的是二分搜尋樹有可能退化成的連結串列這種情況。因此引入平衡因子這個概念。AVL同時也是一個二分搜尋樹。所以也要滿足二分搜尋樹的性質。

在後續為AVL樹新增自平衡機制時, 如果程式碼有bug, 就很有可能破壞這個性質, 所以設定一個方法用來判斷當前AVL樹是否還是一顆二分搜尋樹。

判斷二叉樹是否為二分搜尋樹


/**
 * 判斷該二叉樹是否是一顆二分搜尋樹
 * @return
 */
public boolean isBST() {
    if (root == null)
        return true;

    /***
     * 在介紹二分搜尋樹的時候, 我們介紹過一個特性, 如果是一顆二分搜尋樹在進行中序遍歷它是升序的
     */
    ArrayList<K> keys = new ArrayList<K>();
    inOrder(root, keys);

    for (int i = 1; i < keys.size(); i ++) {
        if (keys.get(i - 1).compareTo(keys.get(i)) > 0) // 如果不是升序的情況則是不是一顆二分搜尋樹。
            return false;
    }

    return true;
}

private void inOrder(Node node, ArrayList<K> keys) {
    if (node == null)
        return ;

    inOrder(node.left, keys);
    keys.add(node.key);
    inOrder(node.right, keys);
}
複製程式碼

判斷二叉樹是否為平衡二叉樹

/***
 * 判斷該二叉樹是否是一顆平衡二叉樹。
 * @return
 */
public boolean isBalanced() {
    return isBalanced(root);
}

private boolean isBalanced(Node node) {
    if (node == null)
        return true; // 如果這棵樹都為空, 肯定的是一個平衡的 /狗頭

    int balanced = getBalanceFactor(node);
    if (Math.abs(balanced) > 1)
        return false;

    return isBalanced(node.left) && isBalanced(node.right); // 左子樹和右子樹平衡因子都必須在範圍1內才是一顆平衡二叉樹
}
複製程式碼
旋轉操作基本原理
左旋轉和右旋轉

AVL是如何實現自平衡的, 在這裡主要有兩個操作, "左旋轉和右旋轉"。AVL樹是在什麼時候維護自平衡的。回憶一下, 我們在二分搜尋樹插入一個節點的時, 我們需要從根節點一路向下最終尋找到正確的插入位置, 那麼, 正確的插入位置都是一個葉子節點。

也就是說, 由於我們新新增了一個節點才有可能導致我們整顆二分搜尋樹不在滿足平衡性。相應的, 這個不平衡的節點只有可能發生在我們插入的位置向父節點去查詢, 因為我們是插入了一個節點才破壞了整顆樹的平衡性。我們破壞的整棵樹的平衡性將反映在這個新的節點的父節點或者祖先節點中。因為在插入這個節點後, 它的父節點或者祖先節點的高度值就需要進行更新。在更新之後平衡因子可能大於1或者小於-1, 也就是左右子樹高度差超過了1。

所以, 我們維護平衡的時機, 應該是, 當我們加入節點後, 沿著節點向上維護平衡性。[參考圖2-1]

[圖2-1 [在什麼時候維護平衡]]

2-1

我們先來看一下不平衡發生的最簡單的一種情況[圖2-2]中圖1的內容。

假設我們現在有一顆空樹, 現在我們新增一個節點12, 那麼此時這個節點的平衡因子就是0。 然後, 我們有新增一個元素8, 8比12小所以在12的左子樹上, 那麼節點8的平衡因子就是0, 相應的12這個節點它的平衡因子就更新為1, 然後我們在新增一個節點5, 由於5比8還小, 一路找下來最終成為8的左子樹。此時5是一個葉子節點, 它的平衡因子為0, 回到父節點8更新平衡因子為1, 而對於祖先節點12它的平衡因子更新為2。

那麼在節點12的位置上, 此時它的平衡因子絕對值大於1, 所以我們需要對它進行一個平衡維護。

再比如說, 我們有 [圖2-2]中圖2中的情況。如果我們在這顆樹上新增一個節點2的話, 這個節點從根節點出發查詢, 一直找到節點4並放置在左子樹下。

新增完節點2之後, 它是一個新節點它的左右子樹都是空, 所以節點2的平衡因子為0。 然後回溯上去到節點2的父節點4, 此時節點4的平衡因子為1, 相應的在往上走對於節點5來說它的平衡因子也為1, 在向上走到節點8它的平衡因子為2, 換句話說, 到節點8的位置打破平衡二叉樹的性質。我們需要對節點8進行一個平衡維護。

我們舉的這兩個例子, 無論是從空樹新增元素還是在已有節點上新增元素, 本質是一樣的。"都是插入的元素在不平衡的節點的左側的左側", 換句話說, 我們一直在向這棵樹的左側新增元素。最終導致左子樹的高度要比右子樹的高度要高。與此同時, 我們觀察這個不平衡節點的左子樹它的平衡因子也是大於0的, 換句話說對於這個不平衡節點它的左孩子這個節點, 也是左子樹的高度大於右子樹的高度。

[圖2-2 [在什麼時候維護平衡]]

2-2

那麼, 上面發生的問題, 我們如何解決呢?
這裡我們通過右旋轉來進行解決。

右旋轉

[圖2-3 [右旋轉]]

2-3

這裡, 我們將要處理的情況抽象如圖[2-3]中第一幅圖的樣子, 我們有一個Y節點, 對於Y節點來說已經不滿足平衡二叉樹的條件了。與此同時, 這裡我們討論的是它的左子樹的高度要比右子樹的高度要高。並且這個高度差是要比1大的。與此同時, 它的左孩子也是同樣的情況。左子樹的高度是大於等於右子樹的。也就是說以Y為根節點這顆子樹, 它整體不滿足平衡二叉樹的性質並且整體是向左傾斜的。

為了不失一般性, 它們的右側可能也有子樹。如圖[2-3]中第二幅圖的樣子, 對於節點Z它的左右是T1和T2。T1和T2可以為空, 只不過不失一般化的處理。讓Z也擁有兩顆子樹。但是Z是一個葉子節點也是完全沒問題的, 同理, 對於節點X它右側可能有子樹T3, 對於節點Y可能也有右子樹T4。

對於圖[2-3]中是Y這個節點左子樹過高, 所以希望經過操作後Y這個節點可以保持平衡。與此同時我們整棵子樹不能失去二分搜尋樹的特性。具體如何實現呢?

我們需要進行右旋轉, 那麼右旋轉的過程是怎樣的呢?

首先讓X的右子樹指向節點Y, 之後我們在讓節點Y的左子樹指向T3。 經過上面的操作之後, X成為根節點, 這樣一個過程稱為右旋轉。
此時, 經過旋轉後得到新的二叉樹它既滿足二分搜尋樹的性質又滿足平衡二叉樹的性質。

這裡我主要說明一下保持平衡二叉樹的性質:

最終圖參考[圖2-3-1], 我們簡單分析一下。

左圖中我們看到Y是不平衡的節點, 也就意味著Z和X為根的二叉樹是平衡二叉樹。不然的話, 我們從加入的節點開始不斷向上回溯,
找到的第一個不平衡的節點就不應該是Y這個節點。

所以依然是以Z為根的二叉樹它是平衡的二叉樹。相應的右圖中以Z為根節點這棵二叉樹保持平衡性, 相當於沒有變化。

如果以Z為根節點保持平衡性的話, T1和T2它們的高度差不會超過1。假設T1和T2最大的高度值是H, 那麼Z這個節點高度值就是H+1

在這裡由於X也是保持平衡的。並且對於X來說它的平衡因子大於等0的, 也就是說左子樹的高度大於等於右子樹的高度。
注意, 由於X也是保持平衡的, 所以X的平衡因子最大為1。也就是說X的平衡因子要麼是0要麼是1。
對應就是T3這棵樹的高度要麼是H要麼是H+1, 這樣一來對於X這個節點來說它的高度值就是H+2。

我們在來看Y這個節點, 這個節點打破了平衡。它的左右子樹高度差是大於1的。但是, 有個點需要注意的是這個高度差最大是2。這是為什麼呢?

這是因為, 我們以Y節點為根的樹新增了一個節點打破了平衡性。原來Y節點是平衡的, 現在我們新增一個節點之後, 如果不平衡了,
這個高度差只有可能為2。而不可能為3。這是因為我們只新增了一個節點, 不可能讓左子樹的高度一下就新增2,
所以在這種情況下由於Y這個節點不平衡了, 那麼肯定是左子樹比右子樹大了2, 所以在這種情況下, T4的高度應該為H。


瞭解了這一點後, 我們在看一下旋轉後的樹, 對於T3這顆子樹來說它的高度要麼是H要麼是H+1, 而T4它的高度是H。所以, 整體來看對於Y這個節點來說也是保持平衡的,
並且在這裡Y這個節點, 它的高度值是H+2或者是H+1的, 具體是誰, 取決於T3的高度,
如果T3的高度是H+1, 那麼Y節點的高度就是H+2, 如果T3的高度是H的話, T3和T4都是H, Y節點的高度值就是H+1。

不管Y這個節點高度是H+1還是H+2, 我們從X這個節點角度來看, X這個節點依然是平衡的。Y和Z兩個節點高度差是不會超過1的。


複製程式碼

[圖2-3-1]

2-3-1

至此, 希望到這裡大家能清楚的明白右旋轉是如何操作的。下面我們開始我們實際編寫右旋轉的程式碼。

右旋轉實現
/****
 *
 *                 y                                                 x
 *               /  \                                              /   \
 *              x    T4           向右旋轉(y)                      z     y
 *             / \             -------------->                  / \    / \
 *            z   T3                                           T1 T2  T3 T4
 *           / \
 *          T1 T2
 *
 * @param y
 * @return
 */
 private Node rightRotate(Node y) {
     /***
      *  根據上面的圖例, 我們進行右旋轉
      */

     // 1. 首先獲取X節點
     Node x = y.left;

     // 2. 獲取X節點的右子樹
     Node t3 = x.right;

     // 3. 更新X節點的右子樹, 把Y節點掛載上
     x.right = y;

     // 4. 更新Y節點的左子樹, 將X之前的右節點資料掛載上去
     y.left = t3;


     /***
      *  注意:
      *    不要忘記更新樹的高度, 當我們旋轉後, 樹的高度就會降低。
      *    更新的順序是先更新Y節點的值, 在更新X節點的值, 這是因為X節點的高度值是會和新的Y節點的高度值相關的:
      *      1. 先更新Y節點的高度
      *      2. 在更新X節點的高度
      */
     y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
     x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;

     return x;
 }
複製程式碼
左旋轉

上面, 我們已經把右旋轉基本介紹完了, 那麼, 與之對應的就是左旋轉了。
如果對右旋轉已經瞭解明白了的話, 相信在學習左旋轉會比較輕鬆的。

右旋轉是我們插入的元素在不平衡節點的左側的左側, 左旋轉對應的要糾正的就是 插入的元素在不平衡節點的右側的右側。

如圖[3-1]

[圖3-1 [進行左旋轉的二叉樹]]

3-1

可以發現以Y為根節點的右子樹的高度值比左子樹高度值相差值大於了1。換句話說, 左子樹的高度減去右子樹的高度小於了-1。

這種情況下, 我們就需要進行左旋轉了。 左旋轉過程其實和右旋轉的操作基本一樣, 只不過是方向上的不同而已

  1. 首先把x的左子樹指向y
  2. 然後讓y的右子樹指向x在指向y之前的左子樹

那麼高過程也是在圖[3-1]中有展示, 包括掛載流程。

以上, 就對左旋轉介紹完畢了, 下面就是左旋轉的程式碼實現。

左旋轉實現
/****
 *
 *                 y                                                 x
 *               /  \                                              /   \
 *              T1   x                 向左旋轉(y)                 y     z
 *                  / \             -------------->             / \    / \
 *                 T2  z                                       T1 T2  T3 T4
 *                    / \
 *                   T3 T4
 *
 * @param y
 * @return
 */
private Node leftRotate(Node y) {
    /***
     *  根據上面的圖例, 我們進行左旋轉
     */

    // 1. 首先獲取X節點
    Node x = y.right;

    // 2. 接收X節點的左子樹
    Node t2 = x.left;

    // 3. 將節點Y掛載到節點X的左子樹下
    x.left = y;

    // 4. 將T2掛載到Y節點的右子樹下
    y.right = t2;

    /***
     *  注意:
     *    不要忘記更新樹的高度, 當我們旋轉後, 樹的高度就會降低。
     *    更新的順序:
     *      1. 先更新Y節點的高度
     *      2. 在更新X節點的高度
     */
    y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
    x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;

    return x;

}
複製程式碼
LR和RL

經過上面的學習, 當我們插入一個節點, 插入的這個節點就可能會引發這個節點的祖先節點的不平衡。

如果我們插入的這個節點, 在我們這個不平衡節點的左側的左側時, 處理的方法是向右旋轉。相對應的是在不平衡節點的右側的右側時, 處理的方法就是向左旋轉。

但是, 我們考慮下圖這種情況: 插入的元素在不平衡節點的左側的右側

4-1

例如左圖中我們新增節點10, 右圖中我們新增節點4的話。此時, 在插入這個節點之後, 向上去尋找祖先節點依然是對於左圖中的節點12和右圖中的節點8開始, 產生了不平衡。

此時, 我們處理的方式就不能單純的只是右旋轉或者左旋轉了。

那麼我們以左圖為例, 如果插入的節點是10, 如果我們只是簡單的向右旋轉的話, 我們讓節點8作為根節點的話是不可以的。這是因為節點10和節點12都比節點8要大。所以, 這種情況下不能單純的只是進行一次右旋轉。相應的, 左旋轉也是同理的。

這種情況下, 我們就應該使用其它的處理方式。

圖[4-2 [LR情況]]

4-2

問:這種情況我們稱為(LR), 什麼意思呢?
答: 就是我們新插入一個節點, 對於這個節點向上去尋找, 尋找到第一個不平衡節點Y, 新插入的節點是在Y這個節點的左孩子的右側(一左一右),所以叫做LR。

對於LR這種情況, 具體如何處理呢?

  1. 首先對X節點進行左旋轉, [這裡注意: 之前我們無論是左旋轉還是右旋轉, 雖然有X,Y,Z三個節點, 但其實我們節點Z是一直沒有動過的, 所以我們左右旋轉最終只有改變兩個節點的值, 那麼在這裡以X為根的樹, 我們就要對X進行左旋轉, 就會改變X,Z兩個節點值。使得樹就會形成箭頭後的值。]

  2. 當我們的樹形成LL後, 我們只需要對節點Y進行右旋轉即可。

和LR與之對應的就是RL,如下圖:

[RL情況]

4-3

那麼, 所謂的RL就是, 我們新增的一個節點後, 從新增節點位置向上回溯, 找到第一個不平衡的點, 那麼, 對於這個不平衡的點來說, 我們新新增的節點是在不平衡節點的右子樹的左側(先右後左)。這種情況就叫做RL。

對於RL處理的方式和我們處理LR的方式, 是完全對稱的, 所謂的完全對稱就是在此時 首先對X進行右旋轉, 右旋轉之後就會形成箭頭後的值, 也就是形成RR的情況, 我們按照RR的情況向左旋轉, 也就是以節點Y為根的樹進行左旋轉。

以上, 我們就介紹完當我們向二分搜尋中新增一個節點, 節點向上回溯, 找到第一個不平衡的節點, 對於這個不平衡的節點來說, 相應的不平衡的情況只有可能是這四種情況, 即LL,RR,LR,RL。

LR和RL的程式碼實現

private Node add(Node node, K key, V value){

if(node == null){
   size ++;
   return new Node(key, value); // 遍歷到最後一個節點返回, 預設高度為1
}

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 // key.compareTo(node.key) == 0
   node.value = value;

/***
*  在新增元素的時候, 我們需要維護一下樹的高度。
*  如何計算高度呢?
*    當前節點 + Max(左子樹高度, 右子樹高度)
*/
node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));

/***
*  有了高度之後, 我們很輕易的獲取到平衡因子
*/
int balanceFactor = getBalanceFactor(node);

// 如果平衡因子大於1, 則破壞了這顆樹的平衡性...
// 這裡暫時先不處理, 先輸出一段話即可。
if (Math.abs(balanceFactor) > 1)
   System.out.println("unbalanced : " + balanceFactor);


/***
*
* 維護平衡
*   之所以在這裡維護平衡是, 節點的元素新增完成了, 也一級一級的向父節點回溯中, 並得到相應的高度和平衡因子。
*   因此可以很方便的知道當前以該節點為根的樹是否保持平衡性。
*/

// 如果需要右旋轉的情況(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;
}
複製程式碼
刪除元素

與之對應的, 當我們刪除一個節點的時候, 樹的高度也會隨之改變, 這個時候, 我們也需要 維護二叉樹的平衡。這裡如何維護平衡呢? 當然和我們做新增的時候是一摸一樣的。

由於上面已經長篇大論的介紹瞭如何處理四種情況, 這裡我就直接上程式碼了, 當然還是會有註釋的。


private Node remove(Node node, K key){

    if( node == null )
        return null;

    /***
     * 這裡, 使用一個變數接住刪除後的Node資訊, 這樣, 如果當前二分搜尋樹的平衡性被破壞, 我們可以進行平衡。
     */
    Node retNode = null;

    if( key.compareTo(node.key) < 0 ){
        node.left = remove(node.left , key);
//            return node;
        retNode = node;
    } else if(key.compareTo(node.key) > 0 ){
        node.right = remove(node.right, key);
//            return node;
        retNode = node;
    } else{   // key.compareTo(node.key) == 0

        // 待刪除節點左子樹為空的情況
        if(node.left == null){
            Node rightNode = node.right;
            node.right = null;
            size --;
//                return rightNode;
            retNode = rightNode;
        }

        // 待刪除節點右子樹為空的情況
        /***
         * 這裡, 還需要注意一點, 之前我們是return返回資料, 所以我們寫成if沒問題, 現在我們使用變數來接收,
         * 下面的過程都會執行一遍, 但我們的條件是互斥的, 所以需要寫成else if ...了
         *
          */
        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 = removeMin(node.right);
             *   這裡有個小bug需要注意一下, 由於我們並沒有維護removeMin方法中二分搜尋樹的平衡
             *   所以, 很有可能會破壞整棵樹的平衡性。
             *
             *   這裡有兩個解決方法:
             *     1. 在removeMin()方法中維護二分搜尋樹的平衡性。
             *     2. 我們在remove()方法中已經新增了整棵樹的自平衡,
             *        這句話已經求出Node右子樹的最小值: Node successor = minimum(node.right);
             *        而removeMin(node.right);要做的事情就是在Node的右子樹中將這個最小值刪除, 而
             *        我們的remove()方法就是刪除以Node為根節點相應的某一個K對應的節點。
             *        所以successor中已經儲存右子樹的最小值了, 使用可以寫成remove(node.right, successor.key)
             */
//            successor.right = removeMin(node.right);
            // +++
            successor.right = remove(node.right, successor.key);
            successor.left = node.left;

            node.left = node.right = null;

//            return successor;

            retNode = successor;

        }
    }

    // 由於是刪除節點, 有可能retNode會獲得空節點, 需要判斷一下
    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; // 將維護後的樹返回回去

}
複製程式碼
結尾

那麼, 基本上我們的平衡二叉樹也就介紹完了。文章內容有點多, 希望大家多看幾遍。

avatar

相關文章