平衡二叉樹(AVL樹),原來如此!!!

qwer1030274531發表於2021-08-25

一、認識平衡二叉樹

前幾天,我們將搜尋二叉樹(也稱為二叉排序樹)講解了,重點講了搜尋二叉樹的插入和刪除操作,由特別是刪除操作,是比較難的知識點。現在我們將繼續在搜尋二叉樹的基礎之上,學習一顆新的數,那就是大名鼎鼎的平衡二叉樹(AVL樹)。在學習平衡二叉樹前,同學們需掌握了搜尋二叉樹的基本操作之後,再來看平衡二叉樹的知識,就會簡單一點哦!!!


前期文章:二叉樹的概念以及搜尋二叉樹。


本期文章:GitHub原始碼連結。


我們前面講了搜尋二叉樹的定義:一個節點的左子節點的關鍵字值小於這個節點,右子節點的關鍵字值大於或等於這個父節點。簡單點說就是左邊的小,右邊的大。現在我們試著將這個陣列插入到搜尋二叉樹,看看是什麼樣子?


int[] array = {1,2,3,4,5,6,7,8}; //升序

1

當我們試著去插入後,會發現,這顆搜尋二叉樹有一點怪怪,如圖:




根據上面的圖,我們可以看出,當我們去對一個本身已經有序的陣列,去插入到搜尋二叉樹中,結果卻跟“連結串列”長得更相似。也就是說,假設我們需要查詢8,這顆搜尋二叉樹的時間複雜度就跟連結串列一樣,是O(N)了。所以怎麼辦??? 就出現了今天的主題:平衡二叉樹。


要想使時間複雜度降下來,我們就得調整這棵樹的“樣子”,使之查詢元素的時間複雜度,跟這棵樹的深度直接掛鉤,直接變為O(logN)。所以我們得從新增元素的時候著手。平衡二叉樹程式碼的整體框架,跟搜尋二叉樹差不多,如下:



class TreeNode {

    public int val;

    public int height;  //新加的成員變數:節點高度

    public TreeNode left;

    public TreeNode right;

    

    public TreeNode(int val) {

        this.val = val;

        this.height = 1; //新節點的初始高度為1

    }

}


public class AVL{

    private TreeNode root; //根結點

    

    public void add(int val) { //插入新的節點

        

    }

    

    public void remove(int val) { //刪除對應的節點

        

    }

    

    public boolean contains(int val) { //查詢是否有該值

        

    }

    

}


二、插入操作

首先,平衡二叉樹與搜尋二叉樹的差別在於:平衡二叉樹,它自己可以自動調節整棵樹的平衡,也叫自平衡機制。 所以在平衡二叉樹裡,有一個概念叫做平衡因子,意思就是: 對於每一個子樹而言,它的左子樹的高度 減去 右子樹的高度 ,差值的絕對值不能超過1;換句話說就是:兩邊高度相減的範圍必須在[-1,1],這個區間呢,才能稱為平衡。這也是我們為什麼在TreeNode類裡面加入了height這個變數的原因。


插入操作,我們只需掌握4種不同情況導致的不平衡。分別是 LL型、RR型、LR型和RL型。


LL型




就如上面這幅圖所示,當我們插入5節點後,計算平衡因子,我們就會發現,在12結點處的平衡因子超過了1,所以我們需要對12這個節點進行調整。正是因為2節點的插入,從而導致了12節點的不平衡,而5節點在12節點的左子樹(8節點)的左子樹(5節點)。所以這種情況就叫LL型。我們稍微將上面的圖再“裝飾”一下,將它們各自的子節點都顯示出來,如下圖:(注:T1~T4,在實際的情況中可能沒有,此時是為了讓大家更好理解如何去進行旋轉操作,才加上的)




LL型動圖




對於LL型,我們需要進行右旋轉操作,同學們根據動圖,自行在紙上畫一下是如何進行連線的,就能更好的理解其中的關係。程式碼圖如下:




最後,我們還要再說一下,旋轉之後,只有兩個節點的高度是需要更改的,就是root和tmp這兩個節點的高度,等於它左右子樹的高度,再加上自己本身的高度值1,就是旋轉之後,這個節點的新高度了。切記:必須先計算root的高度之後,才能計算tmp的高度,因為root是tmp的右子樹,tmp的高度是依賴於root的高度值。


RR型


講完了LL型,RR型,也就簡單了許多,RR型和LL型是差不多的。只是二者互為映象而已。我們就直接看動圖吧!


RR型動圖


計算高度的節點還是root和tmp這兩個節點,還是先計算root的高度,再計算tmp的高度。程式碼圖如下:




LR型


講完了LL型和RR型,接來了的LR和RL型,就非常簡單,因為LR和RL,根本不需要重新再寫新的方法,我們只需要旋轉兩次,就是LR或者RL的操作,多的不說,我們以圖為切入點,展開來講;




上圖就是LR型的情況,當我們嘗試著上面的LL型和RR型,發現是解決不了問題的。我們只有想辦法讓LR型轉化成LL型,問題就迎刃而解了。問題在於怎麼轉化? 來,我們看下圖:




我們可以發現,我們只需將8節點先向左旋轉一下,就能得到LL型的狀態。得到LL型後,問題就回到了LL型上,那我們再整體向右旋轉一次,就能達到平衡的效果。我們以動圖來演示一下:


LR型動圖



RL型


相應的RL型,跟LR型也是差不多,就是映象而已。LR是先左旋轉再右旋轉,而RL是先有旋轉再左旋轉。我們還是以圖來展開說明吧!




旋轉過程如下:




整體的程式碼演示,我先以圖片的形式,將框架分出來,看著更為直觀一點,更容易理解一點。




public void add(int val) {

    root = add(root, val);

}


private TreeNode add(TreeNode node, int val) {

    if (node == null) {

        return new TreeNode(val);

    }


    if (val < node.val) {

        node.left = add(node.left, val);

    } else { //大於等於的情況,還是需要新建節點

        node.right = add(node.right, val);

    }


    //計算當前節點的高度

    node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1; //取左右兩邊的最大值,再加1


    //計算平衡因子

    int balanceFactor = getBalanceFactor(node);

    if (Math.abs(balanceFactor) > 1) {

        //LL型,做右旋轉處理

        if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {

            return R_Rotate(node); //直接將新的根結點返回即可

        }


        //RR型,做左旋轉處理

        if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {

            return L_Rotate(node); //新的根節點,直接返回

        }


        //LR型,先對左子樹進行左旋轉,然後再對根節點進行右旋轉

        if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {

            node.left = L_Rotate(node.left); //先進行左旋轉,變成LL型

            return R_Rotate(node); //再進行右旋轉

        }


        //RL型,先對右子樹進行右旋轉,然後再對根節點進行左旋轉

        node.right = R_Rotate(node.right);

        return L_Rotate(node);

    }

    return node;

}


//計算平衡因子

private int getBalanceFactor(TreeNode node) {

    if (node == null) {

        return 0;

    }

    return getHeight(node.left) - getHeight(node.right);

}

//計算節點的高度

private int getHeight(TreeNode node) {

    if (node == null) {

        return 0;

    }

    return node.height;

}


這樣的話,平衡二叉樹的插入操作,我們就講完了。接下來,我們來看看刪除操作。


三、刪除操作

刪除和插入是一樣的,我們只是在搜尋二叉樹的刪除操作上,做一些改動,就能實現平衡二叉樹的刪除操作。


分析:整棵樹原本是已經平衡了,是因為我們需要刪除一個節點,從而導致整棵樹產生不平衡。所以們只需要從刪除的節點處,向上一直遍歷,一直向上回溯,然後計算回溯到的節點,重新計算高度,重新計算平衡因子即可。操作完全就是add方法的一樣,直接拷下來即可。下圖是搜尋二叉樹的刪除操作:




平衡二叉樹的刪除,簡直就是一模一樣。我們可以發現,在上面圖中刪除之後,就是直接返回了node節點,。


而平衡二叉樹刪除,就是不要先返回node節點,先對node節點進行計算height,並且計算平衡因子,如果平衡因子超過1了,就調整即可。如果平衡因子沒超過1,此時 返回node節點就行。


平衡二叉樹刪除操作程式碼大致框架如下:




public void remove(int val) {

    root = remove(root, val); //方法過載

}


private TreeNode remove(TreeNode node, int val) {

    if (node == null) {

        return null;

    }

    

    if (val < node.val) { //小於

        node.left = remove(node.left, val);

    } else if (val > node.val) { //大於

        node.right = remove(node.right, val);

    } else if (node.left != null && node.right != null) { //相等的情況,並且有左右兩個孩子

        

        TreeNode minNode = getMinNode(node.right); //返回的是,node的右子樹的最小節點

        minNode.right = remove(node.right, minNode.val); //以這個最小節點作為新的node返回,並刪除右子樹上的minNode

        minNode.left = node.left;

        

        node =  minNode; //這裡先將minNode儲存到node裡面

        

    } else { //相等的情況,只有一個孩子節點,或者是沒有節點情況

        node = node.left != null? node.left : node.right;

    }

    

    

    //對node節點進行判斷,並計算height和平衡因子。

    if (node == null) {

        return null; //上面的else中,有可能node沒有孩子節點,可能會產生null

    }

    

    

     //計算當前節點的高度

    node.height = Math.max(getHeight(node.left), getHeight(node.right)) + 1; //取左右兩邊的最大值,再加1

    //計算平衡因子

    int balanceFactor = getBalanceFactor(node);

    if (Math.abs(balanceFactor) > 1) {

        //LL型,做右旋轉處理

        if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0) {

            return R_Rotate(node); //直接將新的根結點返回即可

        }


        //RR型,做左旋轉處理

        if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0) {

            return L_Rotate(node); //新的根節點,直接返回

        }


        //LR型,先對左子樹進行左旋轉,然後再對根節點進行右旋轉

        if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {

            node.left = L_Rotate(node.left); //先進行左旋轉,變成LL型

            return R_Rotate(node); //再進行右旋轉

        }


        //RL型,先對右子樹進行右旋轉,然後再對根節點進行左旋轉

        node.right = R_Rotate(node.right);

        return L_Rotate(node);

    }

    

   

    return node;

}

//返回這棵樹最小的節點

private TreeNode getMinNode(TreeNode node) {

    TreeNode pre = null;

    while (node != null) {

        pre = node;

        node = node.left; //向左子樹查詢

    }

    return pre;

}


對於平衡二叉樹,我們只需深刻理解到LL型和RR型的旋轉操作,那基本上平衡二叉樹就還算掌握的不錯。LR型和RL型,就是前面兩種的衍生而已。不管是add方法還是remove方法;都只需在搜尋二叉樹的基礎之上進行稍微的修改即可。最後大家可以自己再新增一下方法,例如:isBST、isBalanceTree等等。


好啦,本期更新就到此結束啦!!!同學們好好的拿出紙筆去畫一畫那4種旋轉過程,那麼恭喜你,平衡二叉樹就掌握的不錯啦!!!


下期見!!!

————————————————

版權宣告:本文為CSDN博主「飛人01_01」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。

原文連結:https://blog.csdn.net/x0919/article/details/119862758

1


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/30239065/viewspace-2788619/,如需轉載,請註明出處,否則將追究法律責任。

相關文章