1. 程式人生 > 實用技巧 >資料結構與演算法(十六):平衡二叉樹

資料結構與演算法(十六):平衡二叉樹

一、什麼是平衡二叉樹

1.概述

平衡二叉樹(AVL樹)是一種帶有平衡條件的二叉搜尋樹。它的特性如下:

  • AVL樹的左右兩個子樹的高度差的絕對值不超過1
  • AVL樹的左右兩個子樹都是一棵平衡二叉樹

舉個例子,如上圖所示:

  • 第一棵樹左樹高2,右樹高1,差值為1,是一顆AVL樹;
  • 第二棵樹左樹高2,右樹高2,差值為0,是一顆AVL樹;
  • 第三棵樹左樹高3,右樹高1,差值為2,不是一顆AVL樹;

紅黑樹就是一直AVL樹。

2.為什麼需要平衡二叉樹

當我們使用二叉排序樹的時候,當連續插入順序的節點的時候就會出現問題。比如,我們插入{1,2,3,4,5}這樣一個數組:

可見該樹左樹節點全為空,比起樹更像單鏈表,這也導致了該樹的插入和查詢速度明顯的下降,查詢速度甚至因為每次多處一個比較左樹的操作導致還不如單鏈表。為了避免這種情況,我們引入的AVL樹。

二、AVL樹左旋轉

1.思路分析

AVL為了避免左右樹高度差超過1,在可能導致這種情況的插入或者刪除操作時會進行旋轉。

我們舉個例子,現在有數列{4,3,6,5,7},當插入8後,現在的得到的排序樹如下圖:

明顯不再是一個AVL樹,所以需要進行左旋轉

  1. 我們以當前根節點值再建立一個新節點newNode

  2. 讓新節點的左子節點指向根節點的左子節點

    newNode.left = root.left

  3. 讓新節點的右子節點指向根節點的右子節點的左子節點

    newNode.right = root.right.left

  4. 把根節點的值換成右子節點的值

    root.val = root.right.val

  5. 把根節點的右子節點指向其右子節點的右子節點

    root.right = root.right.right

  6. 讓根節點的左子節點指向新節點(根節點的右子節點成為了新的根節點)

    root.left = newNode

我們調整一下圖片樣式,就可以直觀的看到左旋轉後樹的樣子:

網上看到一個非常形象直觀的動圖:

不難理解:左旋的目的是降低左子樹的高度

2.程式碼實現

由於AVL樹是基於BST改進的一種資料結構,所以這裡的AVL樹類繼承了BST的方法和程式碼,使用同一個節點類,這裡具體的程式碼可以參考之前的文章

我們先建立一個繼承BST的AVL樹類:

/**
 * @Author:CreateSequence
 * @Date:2020-07-23 19:01
 * @Description:平衡二叉樹
 * 由於是在二叉排序樹的基礎上改進,這裡直接繼承了二叉排序樹類
 */
public class AVLTree extends BinarySortTree{

    public AVLTree(BinarySortTreeNode root) {
        super(root);
    }
    
}

由於旋轉的條件是左右子樹高度差大於1,所以我們需要有幾個方法來判斷樹的高度:

/**
 * 獲取當前節點的右子樹高度
 * @param node
 * @return
 */
public int getRightHeight(BinarySortTreeNode node) {
    if (node.right == null) {
        return 0;
    }
    return getHeight(node.right);
}

/**
 * 獲取當前節點的左子樹高度
 * @param node
 * @return
 */
public int getLeftHeight(BinarySortTreeNode node){
    if (node.left == null) {
        return 0;
    }
    return getHeight(node.left);
}

/**
 * 獲取以當前節點為根節點的樹高度
 * @param node
 * @return
 */
public int getHeight(BinarySortTreeNode node) {
    //判斷當前節點的左/右節點是否為空,是返回0,否則遍歷返回當前節點的左右樹最高值
    return Math.max(node.left == null ? 0 : getHeight(node.left), node.right == null ? 0 : getHeight(node.right)) + 1;
}

接著我們需要一個讓樹左旋的程式碼,步驟同思路分析:

/**
 * 排序樹左旋轉
 */
private void leftRotate() {
    // 1.建立新節點,與根節點值相同
    BinarySortTreeNode node = new BinarySortTreeNode(root.val);
    //2.讓新節點左子節點指向根節點左子節點
    node.left = root.left;
    //3.讓新節點的右子節點指向根節點的右子節點的左子節點
    node.right = root.right.left;
    //4.讓根節點的值變為其右子節點的值
    root.val = root.right.val;
    //5.把根節點的右子節點指向其右子節點的右子節點
    root.right = root.right.right;
    //6.讓根節點的左子節點指向新節點
    root.left = node;
}

然後我們再原先舊的新增方法上進行改進:

當新增完一個節點後,我們判斷左右子樹的高度差是否大於1,如果是就進行左旋

/**
 * 重寫二叉排序樹的節點新增方法,當新增完節點後左子樹與右子樹高度差大於1時,讓樹進行左旋轉,若情況相反則進行右旋轉
 * @param node
 */
@Override
public void add(BinarySortTreeNode node) {
    super.add(node);
    //新增完節點後,判斷左子樹與右子樹高度差是否大於1
    int disparity = getRightHeight(root) - getLeftHeight(root);
    if (disparity > 1) {
        System.out.println("高度差:" + disparity + ",左旋轉!");
        //左子樹與右子樹高度差大於1就左旋
        leftRotate();
    }
}

注意:截止目前,僅僅只對左子樹高度較高的情況作了處理!

三、AVL樹的雙旋轉

左旋轉是為了降低左子樹的高度,但是如果是右子樹高度過高,我們就需要右旋,事實上,一個完整的AVL樹,應當是能夠雙旋的。

右旋的步驟與左旋基本一致,但是方向不同:

  1. 我們以當前根節點值再建立一個新節點newNode

  2. 讓新節點的右子節點指向根節點的右子節點

    newNode.right = root.right

  3. 讓新節點的左子節點指向根節點的左子節點右子節點

    newNode.left = root.left.right

  4. 把根節點的值換成左子節點的值

    root.val = root.left.val

  5. 把根節點的左子節點指向其左子節點左子節點

    root.left = root.left.left

  6. 讓根節點的右子節點指向新節點(根節點的左子節點成為了新的根節點)

    root.right = newNode

實現程式碼:

/**
 * 排序樹右旋轉
 */
private void rightRotate() {
    // 1.建立新節點,與根節點值相同
    BinarySortTreeNode node = new BinarySortTreeNode(root.val);
    //2.讓新節點右子節點指向根節點右子節點
    node.right = root.right;
    //3.讓新節點的左子節點指向根節點的左子節點的右子節點
    node.left = root.left.right;
    //4.讓根節點的值變為其左子節點的值
    root.val = root.left.val;
    //5.把根節點的左子節點指向其左子節點的左子節點
    root.left = root.left.left;
    //6.讓根節點的右子節點指向新節點
    root.right = node;
}

現在為排序樹的add方法新增右旋的情況:

/**
 * 重寫二叉排序樹的節點新增方法,當新增完節點後左子樹與右子樹高度差大於1時,讓樹進行左旋轉,若情況相反則進行右旋轉
 * @param node
 */
@Override
public void add(BinarySortTreeNode node) {
    super.add(node);
    //新增完節點後,判斷左右樹高度差是否大於1
    int disparity = getRightHeight(root) - getLeftHeight(root);
    if (disparity > 1) {
        System.out.println("高度差:" + disparity + ",左旋轉!");
        //左子樹與右子樹高度差大於1就左旋
        leftRotate();
    }else if (- disparity > 1){
        //右子樹與左子樹高度差小於1就左旋
        rightRotate();
    }
}