1. 程式人生 > 其它 >AVL平衡二叉樹

AVL平衡二叉樹

AVL 平衡二叉樹

介紹

AVL 樹:Adelson-Velsky and Landis Tree。是電腦科學中最早被髮明的自平衡二叉樹,在 AVL 樹中,任意兩個節點之間的高度差不會超過 1,因此 AVL 樹也被稱之為高度平衡樹。AVL 樹的插入、刪除、查詢操作在平均和最壞的時間複雜度都為 \(O(log_2N)\) 級別的時間複雜度

在這裡,我們給定 AVL 樹的高度差閾值為 1,當有節點的高度差大於這個閾值時,執行相應的操作以實現樹的平衡。

平衡的實現

AVL 樹的平衡也是通過旋轉節點來實現平衡的,分別對以下四種情況進行討論

  • 左左情況

    這種情況是指當前處理的節點的左子節點的左子節點的節點高度差大於給定閾值的情況,如下圖所示:

    如圖所示,當前的 10 節點和底部的 3節點之間的高度差已經大於我們給定的閾值 1了,因此這種情況下需要對 10 節點進行一次右旋轉,使得這棵樹依舊是滿足給定的高度差閾值的。

    進行右旋轉之後:

  • 右右情況

    這種情況就是當前節點的右子節點的右子節點之間節點的高度差大於給定的閾值的情況,如下圖所示:

    在這種情況下,對當前節點進行一次左旋轉即可實現樹的重新平衡,使得當前節點的高度差在給定的閾值範圍內

    進行左旋轉之後:

  • 左右情況

    這種情況下,是由於當前節點與當前節點的左子節點的右子節點之間的高度差達到指定閾值的情況,如下圖所示:

    這種情況下,首先需要對當前節點的左子節點進行一次左旋轉,然後再進行一次右旋轉即可再次達到樹的平衡

    首先對當前節點的左子節點進行左旋轉:

    然後再對當前節點進行一次右旋轉:

  • 右左情況

    這種情況是當前節點和當前節點的右子節點的左子節點之間的節點高度差達到了給定的閾值,這種情況如下圖所示:

    這種情況下,需要首先對當前節點的右子節點進行一次右旋轉,再對當前節點進行一次左旋轉來完成樹的重新平衡

    首先對當前節點的右子節點進行一次右旋轉:

    再對當前節點進行一次左旋轉即可:

以上就是在處理 AVL樹的過程中可能會遇到的幾種情況,通過分析這幾種情況並提供對應的解決策略使得整個樹最終保持平衡,這是理解 AVL樹的基礎。

實現

對於一棵樹來講,最基本的操作便是增、刪、改、查四個操作。查詢操作與一般的二叉樹的查詢沒有任何不同,因此在這裡不介紹具體的實現思路;對於修改操作來講,無非就是找到要修改的節點,然後再更新對應的節點資料即可,這與查詢操作沒有太大的差異。

AVL 樹中最關鍵的兩個操作便是插入和刪除,同時也是實現起來比較困難的地方。主要是要結合上文提到的幾種情況進行分析,然後進行旋轉操作,這幾個過程比較複雜,因此在這裡講述一下實現的思路。

首先,定義 AVL 樹的節點類:

/**
* AVL 樹的節點物件
*/
private static class Node implements Comparable<Node> {
    private int val;
    private Node left, right;
    private int height = 0; // 當前節點的樹高度

    private Node(int val, Node left, Node right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }

    @Override
    public int compareTo(Node o) {
        if (null == o) return 1;

        return this.val - o.val;
    }

    @Override
    public boolean equals(Object obj) {
        if (null == obj) return false;
        if (obj.getClass() != this.getClass())
            return false;

        Node o2 = (Node) obj;

        return this.val == o2.val;
    }

    @Override
    public String toString() {
        return "val: " + val;
    }
}

插入操作的實現

實現思路:遞迴地查詢當前插入的節點的插入位置,將其插入後再檢測當前的處理節點與其相關的子節點之間是否存在高度差達到指定閾值的節點,如果存在這樣的節點,那麼就需要調整這個節點。

重新調整當前的處理節點:

/**
* 重新調節當前的節點樹
* @param parent : 當前處理的要重新調節平衡的節點
* @param node : 插入的節點
* @return : 重新調節之後的得到的根節點
*/
private Node reBalance(Node parent, Node node) {
    parent.height = Math.max(getHeight(parent.left), getHeight(parent.right)) + 1;
    /*
            當左右子樹的高度差達到指定閾值時,需要進行相應的調整
         */
    if (getHeight(parent.left) - getHeight(parent.right) == factor) {
        if (less(node, parent.left))
            /*
                   此時的情況如下所示(帶 ? 表示該節點可能存在):node 節點在 left 節點的左邊,此時的情況如 左左情況一致
                   因此,只需將根節點進行一次右旋轉即可。
                                parent
                               /      \
                             left     right?
                            /    \
                          left?  right?
                          /   \
                       node?  node? (node 的位置不確定可能在左邊,可能在右邊,甚至可能在父節點 left)
                */
            parent = rrRotate(parent, parent.right);
        else
            /*
                   此時的情況如下所示(帶 ? 表示該節點可能存在):node 節點在 left 節點的右邊,此時的情況如 左右情況一致
                   此時,需要先對左子節點進行一次左旋轉,然後對根節點進行一次右旋轉
                                parent
                               /      \
                             left     right?
                            /    \
                          left?  right?
                                 /    \
                               node?  node? (node 的位置不確定可能在左邊,可能在右邊,甚至可能在父節點 right)
                 */
            parent = lrRotate(parent, parent.left);
    } else if (getHeight(parent.right) - getHeight(parent.left) == factor) {
        if (less(node, parent.right))
            /*
                   此時的情況如下所示(帶 ? 表示該節點可能存在):node 節點在 right 節點的左邊,此時的情況如 右左情況一致
                   此時,需要先對右子節點進行一次右旋轉,然後對根節點進行一次左旋轉
                                parent
                               /      \
                             left?    right
                                     /    \
                                   left?  right?
                                   /    \
                                 node?  node? (node 的位置不確定可能在左邊,可能在右邊,甚至可能在父節點 left)
                */
            parent = rlRotate(parent, parent.right);
        else
            /*
                   此時的情況如下所示(帶 ? 表示該節點可能存在):node 節點在 right 節點的右邊,此時的情況如 右右情況一致
                   因此,對根節點執行一次左旋轉即可
                                parent
                               /      \
                             left?    right
                                     /    \
                                   left?  right?
                                          /    \
                                        node?  node? (node 的位置不確定可能在左邊,可能在右邊,甚至可能在父節點 right)
                */
            parent = llRotate(parent, parent.right);
    }

    return parent;
}

每次只要遞迴地調整處理節點即可實現樹整體的調節。

具體的插入操作的實現如下:

 /**
 * 插入一個節點,返回插入後的根節點,如果插入重複元素,則會覆蓋原有的節點
 * @param node : 待插入的節點元素
 * @return : 插入後的根節點
 */
public Node insert(Node node) {
    // 根節點為 null, 表示當前樹還沒有節點,因此將該節點置為根節點
    if (null == root) {
        root = node;
        root.height = 1;
        return root;
    }

    // 遞迴插入該節點,同時更新根節點
    root = insert(node, root);
    return root;
}

private Node insert(Node node, Node parent) {
    if (parent == null) {
        parent = node;
        return parent;
    }

    if (less(node, parent)) {
        parent.left = insert(node, parent.left);
    } else {
        parent.right = insert(node, parent.right);
    }

    return reBalance(parent, node);
}

刪除操作的實現

刪除操作是一個比較複雜的實現,因為如果任意刪除一個節點元素的話,那麼樹的平衡性就很難得到保障。相比較與紅黑樹使用多個節點組成 3- 節點或者 4- 節點來刪除鍵來維護樹的平衡性,AVL 樹的做法就更加純粹一些。

主要有兩種方式來實現節點的刪除操作:一是把要刪除的節點不斷地旋轉,將它旋轉到葉子節點然後再刪除它,然後再重新調整數的平衡;二是從當前節點的左子節點找到最大的元素節點或者從右子節點中找到最小的元素節點來填充當前節點的資料,然後再向下遞迴地刪除這個節點元素,再重新調整樹的平衡性。

想比較而言,使用第二種方式是一個更加簡單有效的實現方式,因此在這裡也採用第二種方式來實現,這裡的實現填充的是右子節點的最小元素資料。

具體實現程式碼如下圖所示:

/**
* 刪除對應的 node 節點,然後返回刪除節點之後的根節點
* @param node:待刪除的節點
* @return : 刪除節點之後的根節點
*/
public Node delete(Node node) {
    return delete(node, root);
}

/**
* 刪除操作的基本流程:
*  1、找到對應的節點,然後使用右子節點的最小節點替換掉當前的節點
*  2、在右子樹中遞迴地刪除使用的替換節點,直到為空
*  3、在刪除的過程中需要重新調整樹
* @param node : 待刪除的節點
* @param parent :當前處理的節點
* @return : 處理完之後的根節點
*/
private Node delete(Node node, Node parent) {
    // 當前處理的節點為空,說明已經到到遞迴終點了,停止遞迴
    if (parent == null)
        return null;

    /*
   	刪除節點之後,需要使用一個別的節點來替換該節點以維持樹的平衡
    	有兩種方案可以選取:左子樹的最右節點和右子樹的最左節點,這裡選用的是第二種
    */
    if (node.equals(parent)) {
        if (parent.right == null) {
            // 右子節點為空,那麼只需要放棄這個節點的引用,使得垃圾收集器刪除即可
            parent = parent.left;
            return parent;
        } else {
            // 查詢右子樹的最左節點,以充當平衡節點
            Node rNode = parent.right;
            while (rNode.left != null)
                rNode = rNode.left;
            // 查詢替換節點結束

            parent.val = rNode.val; // 更新當前的待刪除節點,此時的樹依舊是平衡的
            // 由於已經將替換節點提上來了,因此需要刪除這個節點
            parent.right = delete(rNode, parent.right);
        }
    } else if (less(node, parent)) {
        parent.left = delete(node, parent.left);
    } else {
        parent.right = delete(node, parent.right);
    }

    // 重新調整樹的平衡
    parent.height = Math.max(getHeight(parent.left), getHeight(parent.right)) + 1;
    if (getHeight(parent.right) - getHeight(parent.left) == factor) {
        if (getHeight(parent.right.right) >= getHeight(parent.right.left))
            /*
                   此時的情況如下所示(帶 ? 表示該節點可能存在):與右右情況一致
                   因此,對根節點執行一次左旋轉即可
                                parent
                               /      \
                             left?    right
                                     /    \
                                   left?  right?
                                          /    \
                                        left?   right?
            */
            parent = llRotate(parent, parent.right);
        else
            /*
                   此時的情況如下所示(帶 ? 表示該節點可能存在):與右左情況一致
                   需要對右子節點執行一次右旋轉,然後對根節點執行一次左旋轉
                                parent
                               /      \
                             left?    right
                                     /    \
                                   left?  right?
                                  /    \
                               left?   right?
                */
            parent = rlRotate(parent, parent.right);

    } else if (getHeight(parent.left) - getHeight(parent.right) == factor) {
        if (getHeight(parent.left.left) >= getHeight(parent.left.right))
            /*
                   此時的情況如下所示(帶 ? 表示該節點可能存在):與左左情況一致
                   對根節點執行一次右旋轉即可
                                parent
                               /      \
                             left    right?
                            /    \
                          left?  right?
                         /    \
                      left?   right?
                */
            parent = rrRotate(parent, parent.left);
        else
            /*
                   此時的情況如下所示(帶 ? 表示該節點可能存在):與左右情況一致
                   需要對左子節點執行一次左旋轉,然後對根節點執行一次右旋轉即可
                                parent
                               /      \
                             left    right?
                            /    \
                          left?  right?
                                 /    \
                              left?   right?
                */
            parent = lrRotate(parent, parent.right);
    }

    return parent;
}

總體上相比較於紅黑樹而言, AVL 樹中需要的操作更多,因此整體效能上要比紅黑樹要差一些。

整體的實現程式碼:https://github.com/LiuXianghai-coder/Test-Repo/blob/master/DataStructure/AVL.java