資料結構與演算法:AVL樹

小高飛發表於2020-10-14

AVL樹

在電腦科學中,AVL樹是最先發明的自平衡二叉查詢樹。在AVL樹中任何節點的兩個子樹的高度最大差別為1,所以它也被稱為高度平衡樹。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。AVL樹得名於它的發明者G. M. Adelson-Velsky和E. M. Landis,他們在1962年的論文《An algorithm for the organization of information》中發表了它。

平衡因子:某結點的左子樹與右子樹的高度(深度)差即為該結點的平衡因子

AVL樹的特點

  1. 本身首先是一棵二叉搜尋樹。

  2. 帶有平衡條件:每個結點的左右子樹的高度之差的絕對值(平衡因子)最多為1。

    也就是說,AVL樹,本質上是帶了平衡功能的二叉查詢樹(二叉排序樹,二叉搜尋樹)

為什麼需要平衡樹?

舉個極端的例子:當我們將一個陣列 [5, 6, 7, 8, 9]轉換成二叉排序樹時,它的二叉樹圖會如下圖所示。

可以看出這時二叉樹趨近於連結串列,雖然它的插入和刪除的效率不會受到影響,但是查詢的效率就有所降低了,因為它和連結串列一樣需要從頭到尾查詢。

資料結構與演算法:AVL樹

 

AVL樹的旋轉操作

AVL樹的旋轉操作有三種:

  • 當右子樹的高度高於左子樹一層以上時進行的左旋轉

  • 當左子樹的高度高於右子樹一層以上時進行的右旋轉

  • 以及特殊情況下進行的雙向旋轉

左旋轉

當二叉排序樹的某一節點的右子樹的高度高於左子樹一層以上時,為了平衡二叉樹排序樹,需要對二叉排序樹進行左旋轉操作。

資料結構與演算法:AVL樹

右旋轉

當二叉排序樹的某一節點的左子樹的高度高於右子樹一層以上時,為了平衡二叉樹排序樹,需要對二叉排序樹進行右旋轉操作。

資料結構與演算法:AVL樹

雙向旋轉

在進行左旋轉或右旋轉時需要判斷一個特殊情況。如下圖所示,根節點的左子樹高度高於右子樹一層以上,即平衡因子大於1,隨即對該二叉樹排序樹進行右旋轉操作,可以 看出右旋轉後根節點右子樹的高度高於左子樹一層以上,平衡因子還是大於1,二叉排序樹的平衡問題還是沒解決。

觀察該二叉排序樹可以發現,根節點的左子節點8號節點的右子樹的高度是高於左子樹的,但只大於1,所以沒有進入到旋轉平衡操作,當我們為根節點下的子樹進行右旋轉時,會將8號節點較高的右子樹連線到10號節點上,這時以10號節點為根節點的樹高度就高於8號節點的左子樹2層了,而隨後我們又將該樹來連線到8號節點的右子樹上,就導致了旋轉後平衡因子還是大於1的問題。

資料結構與演算法:AVL樹

所以在遇到上面這種情況,即左旋轉時右子樹的左子樹高於右子樹的右子樹右旋轉時左子樹的右子樹高於左子樹的左子樹,需先對較高的子樹樹進行一次右旋轉或左旋轉,再對整顆樹進行左旋轉或右旋轉。

下圖是當右旋轉時左子樹的右子樹高於左子樹的左子樹的情況。

資料結構與演算法:AVL樹

資料結構與演算法:AVL樹

 

程式碼實現

獲取樹的高度

在進行旋轉前,需要先判斷以該節點為根節點的樹是否需要進行旋轉平衡,而判斷是否需要旋轉得知道該節點的左子樹和右子樹的高度。

//獲取該節點左子樹的高度
public int leftHeight(){
    if (this.left == null){
        return 0;
    }
    return this.left.height();
}

//獲取該節點右子樹的高度
public int rightHeight(){
    if (this.right == null){
        return 0;
    }
    return this.right.height();
}

//獲取以該節點為根節點的樹的高度
public int height(){
    /*
    Math.max方法可以取出倆個引數中的最大值,因為是遞迴疊加,所以需+1,
    如當該節點是葉子節點時,如果不+1的話,會返回0,導致該節點沒有被算入高度中
     */
    return Math.max(this.left==null ? 0:this.left.height(), this.right==null ? 0:this.right.height()) + 1;
}

旋轉操作

//左旋轉
public AVLNode leftRotate(){
    //儲存該節點的右子樹
    AVLNode rightSubTree = this.right;
    //將該節點的右子樹 替換成 儲存的右子樹rightSubTree的左子樹
    this.right = rightSubTree.left;
    //再把以該節點為根節點的樹 連線到 rightSubTree的左子樹
    rightSubTree.left = this;
    //最後返回左旋轉後新的樹
    return rightSubTree;
}

//右旋轉
public AVLNode rightRotate(){
    AVLNode leftSubTree = this.left;
    this.left = leftSubTree.right;
    leftSubTree.right = this;
    return leftSubTree;
}

在進行完上面步驟後,還需要進行關鍵的一步,把旋轉後返回的新樹連線到該節點在旋轉前的父節點,因為本來以該節點為根節點的樹經過旋轉後根節點發生了變化,不再是該節點,所以需要用旋轉後新樹的根節點去替代旋轉前該節點位置。

在進行連線父節點操作時,還需注意旋轉前該節點是整顆二叉排序樹中一顆子樹的根節點還是整顆二叉排序樹的根節點

新增操作中的平衡判斷:

/**
* 在二叉排序樹中新增節點
* @param node 新增的節點
* @param pointer 指標節點,用於遍歷節點,初始指向根節點
* @return 返回新增後平衡的新樹
*/
public AVLNode add(AVLNode node, AVLNode pointer){
    if (node == null){
        return null;
    }

    if (pointer.value > node.value){//指標節點值大於新增節點值時
        //如果指標節點的左節點剛好為空,則將新增節點插入到該左節點
        if (pointer.left == null){
            pointer.left = node;
        }else {
            //如果不是則繼續往左節點走
            pointer.left = add(node, pointer.left);
        }
    }else {//指標節點值小於新增節點值時
        //如果指標節點的右節點剛好為空,則將新增節點插入到該右節點
        if (pointer.right == null){
            pointer.right = node;
        }else {
            //如果不是則繼續往右節點走
            pointer.right = add(node, pointer.right);
        }
    }

    //新增完節點後,判斷以該節點為根節點的樹是否平衡,如不平衡則需進行旋轉
    if (rightHeight(pointer) - leftHeight(pointer) > 1){//判斷節點的右子樹高度是否比左子樹高過1層以上
        //當左旋轉時,需判斷右子樹的左子樹高度是否高於右子樹的右子樹
        if (leftHeight(pointer.right) > rightHeight(pointer.right)){
            //當滿足上面條件時,則需雙向旋轉
            //先對右子樹進行右旋轉
            AVLNode rightSubTree = rightRotate(pointer.right);
            //將右旋轉後新的右子樹連線到該節點的右子樹位置
            pointer.right = rightSubTree;
            //再對以該節點為根節點的樹進行左旋轉
            pointer = leftRotate(pointer);
        }else {
            //如果不滿足條件,則直接左旋轉
            pointer = leftRotate(pointer);
        }
    }else if (leftHeight(pointer) - rightHeight(pointer) > 1) {//判斷節點的左子樹高度是否比右子樹高過1層以上
        //當右旋轉時,需判斷左子樹的右子樹高度是否高於左子樹的左子樹
        if (rightHeight(pointer.left) > leftHeight(pointer.left)) {
            //當滿足上面條件時,則需雙向旋轉
            //先對左子樹進行左旋轉
            AVLNode leftSubTree = leftRotate(pointer.left);
            //將左旋轉後新的左子樹連線到該節點的左子樹
            pointer.left = leftSubTree;
            //再對以該節點為根節點的樹進行右旋轉
            pointer = rightRotate(pointer);
        } else {
            //如果不滿足條件,則直接右旋轉
            pointer = rightRotate(pointer);
        }
    }
    return pointer;
}

然後將原先的二叉排序樹更新為新增後經過平衡的新的AVL二叉排序樹

//新增節點
public void add(AVLNode node){
    //如果根節點為空則,則將傳入節點設定為根節點
    if (root == null){
        root = node;
    }else {
        root = add(node, root);
    }
}

刪除操作中的平衡判斷:

/**
 * 根據value值刪除節點
 * 刪除節點可能有的3種狀態:
 * 1.該節點是葉子節點
 * 2.該節點只有左子樹或只有右子樹
 * 3.該節點左子樹和右子樹都有
 * @param value 要刪除節點的value值
 */
public AVLNode delete(int value, AVLNode node){
    AVLNode delRes = null;//儲存刪除後新的樹
    if (value < node.value){//當查詢節點值小於當前節點值
        //向左子樹遞迴遍歷,並將刪除後的新的左子樹連線到左節點位置代替原先左子樹
        node.left = delete(value, node.left);
        //刪除後新的樹
        delRes =  node;
    }else if(value > node.value){//當查詢節點值大於當前節點值
        //向右子樹遞迴遍歷,並將刪除後的新的右子樹連線到右節點位置代替原先右子樹
        node.right = delete(value, node.right);
        //刪除後新的樹
        delRes =  node;
    }else {//當查詢節點值等於當前節點值時,即當前節點就是要刪除的節點
        //刪除節點時葉子節點的狀態
        if (node.left == null && node.right == null) {
            //直接將該節點設為空
            //當該節點為空時,無需進入到後面的平衡判斷,直接返回
            return null;
        }
        //刪除節點左子樹為空,右子樹不為空的狀態
        else if (node.left == null && node.right != null) {
            //儲存刪除節點的右子樹
            AVLNode rightSubTree = node.right;
            //將刪除節點的右子樹設為空,使得該節點能夠儘早被垃圾回收
            node.right = null;
            //刪除節點的右子樹,用於連線到刪除節點的父節點
            delRes = rightSubTree;
        }
        //刪除節點右子樹為空,左子樹不為空的狀態
        else if (node.right == null && node.left != null) {
            AVLNode leftSubTree = node.left;
            node.left = null;
            delRes = leftSubTree;
        }
        //刪除節點的左子樹和右子樹都不為空的狀態
        //這裡我們使用的是左子樹的最大值節點代替的方法
        else {
            //獲取左子樹的最大值節點並從左子樹中刪除它
            AVLNode max = max(node.left);
            //將該最大值節點的左子樹和右子樹設定為該節點的左子樹和右子樹
            //注:刪除最大值節點操作的返回值是刪除後該節點的左子樹
            max.left = delMax(node.left);
            max.right = node.right;
            //將刪除節點的左子樹和右子樹設為空,使得該節點能夠儘早被垃圾回收
            node.left = null;
            node.right = null;
            //執行完刪除操作後,以最大值節點為根節點的新的樹,用於連線的刪除節點的父節點
            delRes = max;
        }
    }

    //刪除節點後要判斷刪除後的新的樹是否平衡,如不平衡需進行旋轉操作
    if (rightHeight(delRes) - leftHeight(delRes) > 1){
        if (leftHeight(delRes.right) > rightHeight(delRes.right)){
            AVLNode rightSubTree = rightRotate(delRes.right);
            node.right = rightSubTree;
            return leftRotate(delRes);
        }else {
            return leftRotate(delRes);
        }
    }else if (leftHeight(delRes) - rightHeight(delRes) > 1) {
        if (rightHeight(delRes.left) > leftHeight(delRes.left)) {
            AVLNode leftSubTree = leftRotate(delRes.left);
            delRes.left = leftSubTree;
            return rightRotate(delRes);
        } else {
            return rightRotate(delRes);
        }
    }
    //返回刪除後的新樹
    return delRes;
}

/**
* 查詢傳入節點樹下value值最大的節點並刪除該節點
* @param node
* @return
*/
public AVLNode delMax(AVLNode node){
    if (node.right != null){
        node.right = delMax(node.right);
        return node;
    }else {
        AVLNode leftSubTree = node.left;
        node.left = null;
        return leftSubTree;
    }
}
/**
* 查詢傳入節點樹下value值最大的節點並放回該節點
* 在二叉排序樹中最大值的節點就是最右葉子節點
* @param node
* @return
*/
public AVLNode max(AVLNode node){
    AVLNode max = node;
    while (max.right != null){
        max = max.right;
    }
    return max;
}

同樣也需要將原先的二叉排序樹更新為刪除後經過平衡的AVL二叉排序樹

//刪除節點
public void delete(int value){
    //判斷刪除節點在二叉排序樹中是否存在
    AVLNode node = searchNode(value);
    if (node == null){
        throw new RuntimeException("二叉排序樹內無對應節點");
    }
    //將刪除後新的二叉排序樹更換掉原先二叉排序樹
    root = delete(value, root);
}

 

完整程式碼

public class AVLTreeDemo {
    public static void main(String[] args) {
        AVLTree avlTree = new AVLTree();
        int[] array = {4,3,6,5,7,8};//左旋轉
        //int[] array = {10,12,8,9,7,6};//右旋轉
        //int[] array = {10,11,7,6,8,9};//雙向旋轉
        for (int i : array){
            avlTree.add(new AVLNode(i));
        }

        System.out.println("leftHeight: " + avlTree.leftHeight(avlTree.root));
        System.out.println("rightHeight" + avlTree.rightHeight(avlTree.root));
        avlTree.midOrder();
    }
}

//AVL二叉排序樹
class AVLTree{
    AVLNode root;

    public void setRoot(AVLNode root){
        this.root = root;
    }

    //左旋轉
    public AVLNode leftRotate(AVLNode node){
        //儲存該節點的右子樹
        AVLNode rightSubTree = node.right;
        //將該節點的右子樹 替換成 儲存的右子樹rightSubTree的左子樹
        node.right = rightSubTree.left;
        //再把以該節點為根節點的樹 連線到 rightSubTree的左子樹
        rightSubTree.left = node;
        //最後返回左旋轉後新的樹
        return rightSubTree;
    }

    //右旋轉
    public AVLNode rightRotate(AVLNode node){
        AVLNode leftSubTree = node.left;
        node.left = leftSubTree.right;
        leftSubTree.right = node;
        return leftSubTree;
    }

    //獲取該節點左子樹的高度
    public int leftHeight(AVLNode node){
        if (node.left == null){
            return 0;
        }
        return height(node.left);
    }

    //獲取該節點右子樹的高度
    public int rightHeight(AVLNode node){
        if (node.right == null){
            return 0;
        }
        return height(node.right);
    }

    //獲取以該節點為根節點的樹的高度
    public int height(AVLNode node){
        /*
        Math.max方法可以取出倆個引數中的最大值,因為是遞迴疊加,所以需+1,
        如當該節點是葉子節點時,如果不+1的話,會返回0,導致該節點沒有被算入高度中
         */
        return Math.max(node.left==null ? 0:height(node.left), node.right==null ? 0:height(node.right)) + 1;
    }

    //新增節點
    public void add(AVLNode node){
        //如果根節點為空則,則將傳入節點設定為根節點
        if (root == null){
            root = node;
        }else {
            root = add(node, root);
        }
    }

    /**
     * 在二叉排序樹中新增節點
     * @param node 新增的節點
     * @param pointer 指標節點,用於遍歷節點,初始指向根節點
     * @return 返回新增後平衡的新樹
     */
    public AVLNode add(AVLNode node, AVLNode pointer){
        if (node == null){
            return null;
        }

        if (pointer.value > node.value){//指標節點值大於新增節點值時
            //如果指標節點的左節點剛好為空,則將新增節點插入到該左節點
            if (pointer.left == null){
                pointer.left = node;
            }else {
                //如果不是則繼續往左節點走
                pointer.left = add(node, pointer.left);
            }
        }else {//指標節點值小於新增節點值時
            //如果指標節點的右節點剛好為空,則將新增節點插入到該右節點
            if (pointer.right == null){
                pointer.right = node;
            }else {
                //如果不是則繼續往右節點走
                pointer.right = add(node, pointer.right);
            }
        }

        //新增完節點後,判斷以該節點為根節點的樹是否平衡,如不平衡則需進行旋轉
        if (rightHeight(pointer) - leftHeight(pointer) > 1){//判斷節點的右子樹高度是否比左子樹高過1層以上
            //當左旋轉時,需判斷右子樹的左子樹高度是否高於右子樹的右子樹
            if (leftHeight(pointer.right) > rightHeight(pointer.right)){
                //當滿足上面條件時,則需雙向旋轉
                //先對右子樹進行右旋轉
                AVLNode rightSubTree = rightRotate(pointer.right);
                //將右旋轉後新的右子樹連線到該節點的右子樹位置
                pointer.right = rightSubTree;
                //再對以該節點為根節點的樹進行左旋轉
                pointer = leftRotate(pointer);
            }else {
                //如果不滿足條件,則直接左旋轉
                pointer = leftRotate(pointer);
            }
        }else if (leftHeight(pointer) - rightHeight(pointer) > 1) {//判斷節點的左子樹高度是否比右子樹高過1層以上
            //當右旋轉時,需判斷左子樹的右子樹高度是否高於左子樹的左子樹
            if (rightHeight(pointer.left) > leftHeight(pointer.left)) {
                //當滿足上面條件時,則需雙向旋轉
                //先對左子樹進行左旋轉
                AVLNode leftSubTree = leftRotate(pointer.left);
                //將左旋轉後新的左子樹連線到該節點的左子樹
                pointer.left = leftSubTree;
                //再對以該節點為根節點的樹進行右旋轉
                pointer = rightRotate(pointer);
            } else {
                //如果不滿足條件,則直接右旋轉
                pointer = rightRotate(pointer);
            }
        }
        return pointer;
    }


    //根據value值查詢節點
    public AVLNode searchNode(int value){
        if (root == null){
            return null;
        }
        return searchNode(value, root);
    }

    /**
     * 根據value值查詢節點
     * @param value 查詢的節點
     * @param node 查詢的樹
     * @return
     */
    public AVLNode searchNode(int value, AVLNode node){
        //如果當前節點的值等於value時,則返回該節點
        if (node.value == value) {
            return node;
        } else if (node.value > value){//當前節點的值大於value時
            //如果該節點的左節點為空,則表示二叉排序樹內沒有該值的節點,返回空
            if (node.left == null)
                return null;
            //左節點不為空,繼續往左子樹查詢
            return searchNode(value, node.left);
        }else {//當前節點的值小於value時
            //如果該節點的右節點為空,則表示二叉排序樹內沒有該值的節點,返回空
            if (node.right == null)
                return null;
            //右節點不為空,繼續往右子樹查詢
            return searchNode(value, node.right);
        }
    }

    public void delete(int value){
        //判斷刪除節點在二叉排序樹中是否存在
        AVLNode node = searchNode(value);
        if (node == null){
            throw new RuntimeException("二叉排序樹內無對應節點");
        }
        //將刪除後新的二叉排序樹更換掉原先二叉排序樹
        root = delete(value, root);
    }

    /**
     * 根據value值刪除節點
     * 刪除節點可能有的3種狀態:
     * 1.該節點是葉子節點
     * 2.該節點只有左子樹或只有右子樹
     * 3.該節點左子樹和右子樹都有
     * @param value 要刪除節點的value值
     */
    public AVLNode delete(int value, AVLNode node){
        AVLNode delRes = null;//儲存刪除後新的樹
        if (value < node.value){//當查詢節點值小於當前節點值
            //向左子樹遞迴遍歷,並將刪除後的新的左子樹連線到左節點位置代替原先左子樹
            node.left = delete(value, node.left);
            //刪除後新的樹
            delRes =  node;
        }else if(value > node.value){//當查詢節點值大於當前節點值
            //向右子樹遞迴遍歷,並將刪除後的新的右子樹連線到右節點位置代替原先右子樹
            node.right = delete(value, node.right);
            //刪除後新的樹
            delRes =  node;
        }else {//當查詢節點值等於當前節點值時,即當前節點就是要刪除的節點
            //刪除節點時葉子節點的狀態
            if (node.left == null && node.right == null) {
                //直接將該節點設為空
                //當該節點為空時,無需進入到後面的平衡判斷,直接返回
                return null;
            }
            //刪除節點左子樹為空,右子樹不為空的狀態
            else if (node.left == null && node.right != null) {
                //儲存刪除節點的右子樹
                AVLNode rightSubTree = node.right;
                //將刪除節點的右子樹設為空,使得該節點能夠儘早被垃圾回收
                node.right = null;
                //刪除節點的右子樹,用於連線到刪除節點的父節點
                delRes = rightSubTree;
            }
            //刪除節點右子樹為空,左子樹不為空的狀態
            else if (node.right == null && node.left != null) {
                AVLNode leftSubTree = node.left;
                node.left = null;
                delRes = leftSubTree;
            }
            //刪除節點的左子樹和右子樹都不為空的狀態
            //這裡我們使用的是左子樹的最大值節點代替的方法
            else {
                //獲取左子樹的最大值節點並從左子樹中刪除它
                AVLNode max = max(node.left);
                //將該最大值節點的左子樹和右子樹設定為該節點的左子樹和右子樹
                //注:刪除最大值節點操作的返回值是刪除後該節點的左子樹
                max.left = delMax(node.left);
                max.right = node.right;
                //將刪除節點的左子樹和右子樹設為空,使得該節點能夠儘早被垃圾回收
                node.left = null;
                node.right = null;
                //執行完刪除操作後,以最大值節點為根節點的新的樹,用於連線的刪除節點的父節點
                delRes = max;
            }
        }

        //刪除節點後要判斷刪除後的新的樹是否平衡,如不平衡需進行旋轉操作
        if (rightHeight(delRes) - leftHeight(delRes) > 1){
            if (leftHeight(delRes.right) > rightHeight(delRes.right)){
                AVLNode rightSubTree = rightRotate(delRes.right);
                node.right = rightSubTree;
                return leftRotate(delRes);
            }else {
                return leftRotate(delRes);
            }
        }else if (leftHeight(delRes) - rightHeight(delRes) > 1) {
            if (rightHeight(delRes.left) > leftHeight(delRes.left)) {
                AVLNode leftSubTree = leftRotate(delRes.left);
                delRes.left = leftSubTree;
                return rightRotate(delRes);
            } else {
                return rightRotate(delRes);
            }
        }
        //返回刪除後的新樹
        return delRes;
    }

    /**
     * 查詢傳入節點樹下value值最大的節點並刪除該節點
     * @param node
     * @return
     */
    public AVLNode delMax(AVLNode node){
        if (node.right != null){
            node.right = delMax(node.right);
            return node;
        }else {
            AVLNode leftSubTree = node.left;
            node.left = null;
            return leftSubTree;
        }
    }
    /**
     * 查詢傳入節點樹下value值最大的節點並放回該節點
     * 在二叉排序樹中最大值的節點就是最右葉子節點
     * @param node
     * @return
     */
    public AVLNode max(AVLNode node){
        AVLNode max = node;
        while (max.right != null){
            max = max.right;
        }
        return max;
    }

    public void midOrder(){
        if (root != null){
            midOrder(root);
        }else {
            System.out.println("二叉順序樹為空,無法遍歷");
        }
    }

    //中序遍歷
    public void midOrder(AVLNode node){
        if (node.left != null){
            midOrder(node.left);
        }
        System.out.println(node);
        if (node.right != null){
            midOrder(node.right);
        }
    }
}

//AVL二叉排序樹節點
class AVLNode{
    int value;
    AVLNode left;
    AVLNode right;

    public AVLNode(int value){
        this.value = value;
    }

    @Override
    public String toString() {
        return "BSTNode{" +
                "value=" + value +
                '}';
    }
}

 

相關文章