【數據結構06】二叉平衡樹(AVL樹)

@java

1、平衡二叉樹定義

平衡二叉樹又稱AVL樹。它能夠是一顆空樹,或者具備如下性質的二叉排序樹:它的左子樹和右子樹的高度之差(平衡因子)的絕對值不超過1且它的左子樹和右子樹都是一顆平衡二叉樹。node

從上面簡單的定義咱們能夠得出幾個重要的信息:編程

  • 平衡二叉樹又稱AVL樹
  • 平衡二叉樹必須是二叉排序樹
  • 每一個節點的左子樹和右子樹的高度差至多爲1。

在定義中提到了樹的高度和深度,我敢確定有不少讀者必定對樹的高度和深度有所誤解!最可愛的誤解就是樹的高度和深度沒有區別,認爲樹的高度就是深度。宜春就忍不了了,必須得嗶嗶幾句...設計模式

樹的高度和深度本質區別:深度是從根節點數到它的葉節點,高度是從葉節點數到它的根節點。數組

二叉樹的深度是從根節點開始自頂向下逐層累加的;而二叉樹高度是從葉節點開始自底向上逐層累加的。雖然樹的深度和高度同樣,可是具體到樹的某個節點,其深度和高度是不同的。併發

其次就是對樹的高度和深度是從1數起,仍是從0數起。固然我也有本身的答案,可是衆說紛紜,博主就不說其對與錯了,就很少嗶嗶了。可是我仍是比較認同這張圖的觀點:在這裏插入圖片描述
可參考:https://www.zhihu.com/question/40286584ide

2、這貨仍是不是平衡二叉樹?

判斷一棵平衡二叉樹(AVL樹)有以下必要條件:學習

條件一:必須是二叉搜索樹。
條件二:每一個節點的左子樹和右子樹的高度差至多爲1。測試

在這裏插入圖片描述
在這裏插入圖片描述

3、平衡因子

很少嗶嗶,平衡因子 = 左子樹深度/高度 - 右子樹深度/高度
在這裏插入圖片描述
對於上圖平衡二叉樹而言:
5的結點平衡因子就是 3 - 2 = 1;
2的結點平衡因子就是 1 - 2 = -1;
4的結點平衡因子就是 1 - 0 = 1;
6的結點平衡因子就是 0 - 1 = -1;

對於上圖非平衡二叉樹而言:
3 的結點平衡因子就是 2 - 4 = -2;
1 的結點平衡因子就是 0 - 1 = -1;
4 的結點平衡因子就是 0 - 3 = -3;
5 的結點平衡因子就是 0 - 2 = -2;
6 的結點平衡因子就是 0 - 1 = -1;

特別注意:葉子結點平衡因子都是爲 0

4、如何保持平衡二叉樹平衡?

在這裏插入圖片描述
因爲普通的二叉查找樹會容易失去」平衡「,極端狀況下,二叉查找樹會退化成線性的鏈表,致使插入和查找的複雜度降低到 O(n) ,因此,這也是平衡二叉樹設計的初衷。那麼平衡二叉樹如何保持」平衡「呢?

不難看出平衡二叉樹是一棵高度平衡的二叉查找樹。因此,要構建跟維繫一棵平衡二叉樹就比普通的二叉樹要複雜的多。在構建一棵平衡二叉樹的過程當中,當有新的節點要插入時,檢查是否因插入後而破壞了樹的平衡,若是是,則須要作旋轉去改變樹的結構。關於旋轉,我相信使用文字描述是很難表達清楚的,仍是得靠經典的兩個圖來理解最好不過了!不要不信噢,固然你能夠嘗試讀下文字描述左旋:

左旋簡單來講就是將節點的右支往左拉,右子節點變成父節點,並把晉升以後多餘的左子節點出讓給降級節點的右子節點。

相信你已經暈了。固然能夠試着看看下面的經典動圖理解!

左旋:
在這裏插入圖片描述
==試着用動態和下面的左旋結果圖分析分析,想象一下,估計分分鐘明白左旋!!!==
在這裏插入圖片描述

//左旋轉方法代碼
    private void leftRotate() {
        //建立新的結點,以當前根結點的值
        Node newNode = new Node(value);
        //把新的結點的左子樹設置成當前結點的左子樹
        newNode.left = left;
        //把新的結點的右子樹設置成帶你過去結點的右子樹的左子樹
        newNode.right = right.left;
        //把當前結點的值替換成右子結點的值
        value = right.value;
        //把當前結點的右子樹設置成當前結點右子樹的右子樹
        right = right.right;
        //把當前結點的左子樹(左子結點)設置成新的結點
        left = newNode;
    }

相應的右旋就很好理解了:
在這裏插入圖片描述
反之就是右旋,這裏就再也不舉例了!

小結:當二叉排序樹每一個節點的左子樹和右子樹的高度差超過1的時候,就須要經過旋轉節點來維持平衡!旋轉又分爲左旋、右旋、雙旋轉。

啥?雙旋轉?是的,顧名思義,在一些添加節點的狀況下旋轉一次是不能達到平衡的,須要進行第二次旋轉,

5、平衡二叉樹插入節點的四種狀況

當新節點插入後,有可能會有致使樹不平衡,而可能出現的狀況就有4種,分別稱做左左,左右,右左,右右

==而所謂的「左」和「右」無非就是表明新節點所插入的位置是左仍是右!==

第一個左右表明位於根節點的左或者右,
第二個左右表明位於 【最接近插入節點的擁有兩個子節點的父節點】 位置的左或者右

==固然針對於第二個左右是我我的的看法,不必定徹底正確。有本身想法的讀者,歡迎留言指正!==

下面以左左爲例,分析一波:
在這裏插入圖片描述
在這裏插入圖片描述
其中要特別注意的是:

右右、左左只須要旋轉一次就能夠平衡。
左右、右左要旋轉兩次才能把樹調整平衡!

==其中旋轉的條件就是:當二叉排序樹每一個節點的左子樹和右子樹的高度差超過1的時候!==

6、平衡二叉樹操做的代碼實現

// 建立AVLTree
class AVLTree {
    private Node root;

    public Node getRoot() {
        return root;
    }

    // 查找要刪除的結點
    public Node search(int value) {
        if (root == null) {
            return null;
        } else {
            return root.search(value);
        }
    }

    // 查找父結點
    public Node searchParent(int value) {
        if (root == null) {
            return null;
        } else {
            return root.searchParent(value);
        }
    }

    // 編寫方法:
    // 1. 返回的 以node 爲根結點的二叉排序樹的最小結點的值
    // 2. 刪除node 爲根結點的二叉排序樹的最小結點
    /**
     *
     * @param node 傳入的結點(當作二叉排序樹的根結點)
     *            
     * @return 返回的 以node 爲根結點的二叉排序樹的最小結點的值
     */
    public int delRightTreeMin(Node node) {
        Node target = node;
        // 循環的查找左子節點,就會找到最小值
        while (target.left != null) {
            target = target.left;
        }
        // 這時 target就指向了最小結點
        // 刪除最小結點
        delNode(target.value);
        return target.value;
    }

    // 刪除結點
    public void delNode(int value) {
        if (root == null) {
            return;
        } else {
            // 1.需求先去找到要刪除的結點 targetNode
            Node targetNode = search(value);
            // 若是沒有找到要刪除的結點
            if (targetNode == null) {
                return;
            }
            // 若是咱們發現當前這顆二叉排序樹只有一個結點
            if (root.left == null && root.right == null) {
                root = null;
                return;
            }

            // 去找到targetNode的父結點
            Node parent = searchParent(value);
            // 若是要刪除的結點是葉子結點
            if (targetNode.left == null && targetNode.right == null) {
                // 判斷targetNode 是父結點的左子結點,仍是右子結點
                if (parent.left != null && parent.left.value == value) { // 是左子結點
                    parent.left = null;
                } else if (parent.right != null && parent.right.value == value) {// 是由子結點
                    parent.right = null;
                }
            } else if (targetNode.left != null && targetNode.right != null) { // 刪除有兩顆子樹的節點
                int minVal = delRightTreeMin(targetNode.right);
                targetNode.value = minVal;

            } else { // 刪除只有一顆子樹的結點
                // 若是要刪除的結點有左子結點
                if (targetNode.left != null) {
                    if (parent != null) {
                        // 若是 targetNode 是 parent 的左子結點
                        if (parent.left.value == value) {
                            parent.left = targetNode.left;
                        } else { // targetNode 是 parent 的右子結點
                            parent.right = targetNode.left;
                        }
                    } else {
                        root = targetNode.left;
                    }
                } else { // 若是要刪除的結點有右子結點
                    if (parent != null) {
                        // 若是 targetNode 是 parent 的左子結點
                        if (parent.left.value == value) {
                            parent.left = targetNode.right;
                        } else { // 若是 targetNode 是 parent 的右子結點
                            parent.right = targetNode.right;
                        }
                    } else {
                        root = targetNode.right;
                    }
                }

            }

        }
    }

    // 添加結點的方法
    public void add(Node node) {
        if (root == null) {
            root = node;// 若是root爲空則直接讓root指向node
        } else {
            root.add(node);
        }
    }

    // 中序遍歷
    public void infixOrder() {
        if (root != null) {
            root.infixOrder();
        } else {
            System.out.println("二叉排序樹爲空,不能遍歷");
        }
    }
}

// 建立Node結點
class Node {
    int value;
    Node left;
    Node right;

    public Node(int value) {

        this.value = value;
    }

    // 返回左子樹的高度
    public int leftHeight() {
        if (left == null) {
            return 0;
        }
        return left.height();
    }

    // 返回右子樹的高度
    public int rightHeight() {
        if (right == null) {
            return 0;
        }
        return right.height();
    }

    // 返回 以該結點爲根結點的樹的高度
    public int height() {
        return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
    }

    //左旋轉方法
    private void leftRotate() {

        //建立新的結點,以當前根結點的值
        Node newNode = new Node(value);
        //把新的結點的左子樹設置成當前結點的左子樹
        newNode.left = left;
        //把新的結點的右子樹設置成帶你過去結點的右子樹的左子樹
        newNode.right = right.left;
        //把當前結點的值替換成右子結點的值
        value = right.value;
        //把當前結點的右子樹設置成當前結點右子樹的右子樹
        right = right.right;
        //把當前結點的左子樹(左子結點)設置成新的結點
        left = newNode;


    }

    //右旋轉
    private void rightRotate() {
        Node newNode = new Node(value);
        newNode.right = right;
        newNode.left = left.right;
        value = left.value;
        left = left.left;
        right = newNode;
    }

    // 查找要刪除的結點
    /**
     *
     * @param value
     *            但願刪除的結點的值
     * @return 若是找到返回該結點,不然返回null
     */
    public Node search(int value) {
        if (value == this.value) { // 找到就是該結點
            return this;
        } else if (value < this.value) {// 若是查找的值小於當前結點,向左子樹遞歸查找
            // 若是左子結點爲空
            if (this.left == null) {
                return null;
            }
            return this.left.search(value);
        } else { // 若是查找的值不小於當前結點,向右子樹遞歸查找
            if (this.right == null) {
                return null;
            }
            return this.right.search(value);
        }

    }

    // 查找要刪除結點的父結點
    /**
     *
     * @param value 要找到的結點的值
     *            
     * @return 返回的是要刪除的結點的父結點,若是沒有就返回null
     */
    public Node searchParent(int value) {
        // 若是當前結點就是要刪除的結點的父結點,就返回
        if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {
            return this;
        } else {
            // 若是查找的值小於當前結點的值, 而且當前結點的左子結點不爲空
            if (value < this.value && this.left != null) {
                return this.left.searchParent(value); // 向左子樹遞歸查找
            } else if (value >= this.value && this.right != null) {
                return this.right.searchParent(value); // 向右子樹遞歸查找
            } else {
                return null; // 沒有找到父結點
            }
        }

    }

    @Override
    public String toString() {
        return "Node [value=" + value + "]";
    }

    // 添加結點的方法
    // 遞歸的形式添加結點,注意須要知足二叉排序樹的要求
    public void add(Node node) {
        if (node == null) {
            return;
        }

        // 判斷傳入的結點的值,和當前子樹的根結點的值關係
        if (node.value < this.value) {
            // 若是當前結點左子結點爲null
            if (this.left == null) {
                this.left = node;
            } else {
                // 遞歸的向左子樹添加
                this.left.add(node);
            }
        } else { // 添加的結點的值大於 當前結點的值
            if (this.right == null) {
                this.right = node;
            } else {
                // 遞歸的向右子樹添加
                this.right.add(node);
            }

        }

        //當添加完一個結點後,若是: (右子樹的高度-左子樹的高度) > 1 , 左旋轉
        if(rightHeight() - leftHeight() > 1) {
            //若是它的右子樹的左子樹的高度大於它的右子樹的右子樹的高度
            if(right != null && right.leftHeight() > right.rightHeight()) {
                //先對右子結點進行右旋轉
                right.rightRotate();
                //而後在對當前結點進行左旋轉
                leftRotate(); //左旋轉..
            } else {
                //直接進行左旋轉便可
                leftRotate();
            }
            return ; //必需要!!!
        }

        //當添加完一個結點後,若是 (左子樹的高度 - 右子樹的高度) > 1, 右旋轉
        if(leftHeight() - rightHeight() > 1) {
            //若是它的左子樹的右子樹高度大於它的左子樹的高度
            if(left != null && left.rightHeight() > left.leftHeight()) {
                //先對當前結點的左結點(左子樹)->左旋轉
                left.leftRotate();
                //再對當前結點進行右旋轉
                rightRotate();
            } else {
                //直接進行右旋轉便可
                rightRotate();
            }
        }
    }

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

}

public class AVLTreeDemo {

    public static void main(String[] args) {      
        int[] arr = { 14, 21, 7, 3, 8, 9 };//任意測試節點數組
        //建立一個 AVLTree對象
        AVLTree avlTree = new AVLTree();
        //添加結點
        for(int i=0; i < arr.length; i++) {
            avlTree.add(new Node(arr[i]));
        }

        //遍歷
        System.out.println("中序遍歷");
        avlTree.infixOrder();

        System.out.println("平衡處理...");
        System.out.println("樹的高度=" + avlTree.getRoot().height()); 
        System.out.println("樹的左子樹高度=" + avlTree.getRoot().leftHeight()); 
        System.out.println("樹的右子樹高度=" + avlTree.getRoot().rightHeight());
        System.out.println("當前的根結點=" + avlTree.getRoot());
    }
}

7、AVL樹總結

一、平衡二叉樹又稱AVL樹。

二、平衡二叉樹查詢、插入、刪除的時間複雜度都是 O(logN)

三、插入節點失去平衡的狀況有4種,左左,左右,右左,右右。

四、右右、左左只須要旋轉一次就能夠平衡,而左右、右左要旋轉兩次才能把樹調整平衡!

五、失去平衡最多也只要旋轉2次,因此,調整平衡的過程的時間複雜度爲O(1)

雖然平衡二叉樹有效的解決了極端相似蛇皮單鏈表的狀況,可是平衡二叉樹也不是完美的,AVL樹最大的缺點就是刪除節點時有可能由於失衡,致使須要從刪除節點的父節點開始,不斷的回溯到根節點,若是這棵AVL樹很高的話,那中間就要判斷不少個節點,效率就顯然變的低下!所以咱們後面將要學習2-3樹以及紅-黑樹,抽空寫嘍....

若是本文對你有一點點幫助,那麼請點個讚唄,你的贊同是我最大的動力,謝謝~

最後,如有不足或者不正之處,歡迎指正批評,感激涕零!若是有疑問歡迎留言,絕對第一時間回覆!

歡迎各位關注個人公衆號,裏面有一些java學習資料和一大波java電子書籍,好比說周志明老師的深刻java虛擬機、java編程思想、核心技術卷、大話設計模式、java併發編程實戰.....都是java的聖經,不說了快上Tomcat車,咋們走!最主要的是一塊兒探討技術,嚮往技術,追求技術,說好了來了就是盆友喔...

在這裏插入圖片描述

相關文章
相關標籤/搜索