1. 程式人生 > >【資料結構06】二叉平衡樹(AVL樹)

【資料結構06】二叉平衡樹(AVL樹)

目錄

  • 一、平衡二叉樹定義
  • 二、這貨還是不是平衡二叉樹?
  • 三、平衡因子
  • 四、如何保持平衡二叉樹平衡?
  • 五、平衡二叉樹插入節點的四種情況
  • 六、平衡二叉樹操作的程式碼實現
  • 七、AVL樹總結

@

一、平衡二叉樹定義

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

從上面簡單的定義我們可以得出幾個重要的資訊:

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

在定義中提到了樹的高度和深度,我敢肯定有很多讀者一定對樹的高度和深度有所誤解!最可愛的誤解就是樹的高度和深度沒有區別,認為樹的高度就是深度。宜春就忍不了了,必須得嗶嗶幾句...

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

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

其次就是對樹的高度和深度是從1數起,還是從0數起。當然我也有自己的答案,但是眾說紛紜,博主就不說其對與錯了,就不多嗶嗶了。但是我還是比較認同這張圖的觀點:


可參考:https://www.zhihu.com/question/40286584

二、這貨還是不是平衡二叉樹?

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

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


三、平衡因子

不多嗶嗶,平衡因子 = 左子樹深度/高度 - 右子樹深度/高度

對於上圖平衡二叉樹而言:
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

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


由於普通的二叉查詢樹會容易失去”平衡“,極端情況下,二叉查詢樹會退化成線性的連結串列,導致插入和查詢的複雜度下降到 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的時候,就需要通過旋轉節點來維持平衡!旋轉又分為左旋、右旋、雙旋轉。

啥?雙旋轉?是的,顧名思義,在一些新增節點的情況下旋轉一次是不能達到平衡的,需要進行第二次旋轉,

五、平衡二叉樹插入節點的四種情況

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

==而所謂的“左”和“右”無非就是代表新節點所插入的位置是左還是右!==

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

==當然針對於第二個左右是我個人的見解,不一定完全正確。有自己想法的讀者,歡迎留言指正!==

下面以左左為例,分析一波:


其中要特別注意的是:

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

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

六、平衡二叉樹操作的程式碼實現

// 建立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());
    }
}

七、AVL樹總結

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

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

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

4、右右、左左只需要旋轉一次就可以平衡,而左右、右左要旋轉兩次才能把樹調整平衡!

5、失去平衡最多也只要旋轉2次,所以,調整平衡的過程的時間複雜度為O(1)

雖然平衡二叉樹有效的解決了極端類似蛇皮單鏈表的情況,但是平衡二叉樹也不是完美的,AVL樹最大的缺點就是刪除節點時有可能因為失衡,導致需要從刪除節點的父節點開始,不斷的回溯到根節點,如果這棵AVL樹很高的話,那中間就要判斷很多個節點,效率就顯然變的低下!因此我們後面將要學習2-3樹以及紅-黑樹,抽空寫嘍....

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

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

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

相關推薦

資料結構06平衡AVL

目錄 一、平衡二叉樹定義 二、這貨還是不是平衡二叉樹? 三、平衡因子 四、如何保持平衡二叉樹平衡? 五、平衡二叉樹插入節點的四種情況 六、平衡二叉樹操作的程式碼實現

演算法與資料結構專場堆是什麼鬼?

什麼是二叉堆? 二叉堆是一種特殊的堆。具有如下的特性: 具有完全二叉樹的特性。 堆中的任何一個父節點的值都大於等於它左右孩子節點的值,或者都小於等於它左右孩子節點的值。 根據第二條特性,我們又可以把二叉堆分成兩類:1、最大堆:父節點的值大於等於左右孩子節點的值。 2、最小堆:父節點的值小於等於左右孩

SDUT 3342 資料結構實驗之三:統計葉子數

Problem Description 已知二叉樹的一個按先序遍歷輸入的字元序列,如abc,,de,g,,f,,, (其中,表示空結點)。請建立二叉樹並求二叉樹的葉子結點個數。 Input 連續輸入多組資料,每組資料輸入一個長度小於50個字元的字串。 Output 輸出

資料結構筆記三、

課程是中國大學MOOC浙江大學出的資料結構。 作為一個數據結構愛好者,我覺得很有必要稍微整理下各章節的筆記,對知識進行梳理。 查詢 首先,老師從“查詢”入手,查詢分為靜態和動態,演示了靜態查詢的例程,並介紹了‘建立哨兵’的思想。而這個例程使用的是普通的順序

資料結構05紅-黑基礎----搜尋Binary Search Tree

目錄 1、二分法引言 2、二叉搜尋樹定義 3、二叉搜尋樹的CRUD 4、二叉搜尋樹的兩種極端情況 5、二叉搜尋樹總結 前言 在【演算法04】樹與二叉樹中,已經介紹

學習筆記平衡AVL簡介及其查詢、插入、建立操作的實現

  目錄 平衡二叉樹簡介: 各種操作實現程式碼:   詳細內容請參見《演算法筆記》P319 初始AVL樹,一知半解,目前不是很懂要如何應用,特記錄下重要內容,以供今後review。   平衡二叉樹簡介: 平衡二叉樹由兩位前

資料結構之---C語言實現平衡AVL

//AVL(自動平衡二叉樹) #include <stdio.h> #include <stdlib.h> typedef int ElemType; //每個結點的平均值 typedef enum {      EH = 0,      LH =

資料結構與演算法-查詢平衡(DSW)

上一節探討了二叉查詢樹的基本操作,二叉查詢樹的查詢效率在理想狀態下是O(lgn),使用該樹進行查詢總是比連結串列快得多。但是,該論點並不總是正確,因為查詢效率和二叉樹的形狀息息相關。就像這樣: 圖1-1給出了3顆二叉查詢樹,它們儲存著相同的資料,但很明顯,圖1-1(A)的樹是最好的。在最壞的情況下,圖A定

平衡AVL建立、查詢、插入操作 《大話資料結構》 c++實現程式碼

//平衡二叉樹,或者稱為AVL樹 #include<iostream> using namespace std; typedef int status; #define true 1 #define false 0 #define LH +1 //左高

資料結構與算法系列----平衡AVL

一:背景 平衡二叉樹(又稱AVL樹)是二叉查詢樹的一個進化體,由於二叉查詢樹不是嚴格的O(logN),所以引入一個具有平衡概念的二叉樹,它的查詢速度是O(logN)。所以在學習平衡二叉樹之前,讀者必須需要了解下二叉查詢樹,具體連結:二叉查詢樹 那麼平衡是什麼意思?我們要求

Java資料結構十四—— 平衡AVL

平衡二叉樹(AVL樹) 二叉排序樹問題分析 左子樹全部為空,從形式上看更像一個單鏈表 插入速度沒有影響 查詢速度明顯降低 解決方案:平衡二叉樹 基本介紹 平衡二叉樹也叫二叉搜尋樹,保證查詢效率較高 它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩棵子樹都是一棵平衡

算法模板

treenode tor bsp res stack ack 算法 == oid 模板: 1.先序遍歷三種方法 1)叠代: class Solution { public: /** * @param root: The root of binary tr

劍指offer搜索轉雙向鏈表,C++實現

pointer 題目 size point nod off log tco public 原創博文,轉載請註明出處!# 題目 輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。要求不能創建任何新的結點,只能調整樹中結點指針的指向。要求不能創建任何新的節點

qbxt!預習

實現 n) ret cit AC 容量 數據類型 AI strong qxbt的老師發消息來說讓自己預習,本來想中考完之後認真學(頹)習(廢) 沒辦法 0. 數據結構圖文解析系列 1. 二叉堆的定義 二叉堆是一種特殊的堆,二叉堆是完全二叉樹或近似完全二叉樹。二叉堆

數據結構三十八平衡AVL

圖1 建立 滿足 技術分享 factor 這也 絕對值 因此 調整   一、平衡二叉樹的定義   平衡二叉樹(Self-Balancing Binary Search Tree或Height-Balanced Binary Search Tree),是一種二叉排序樹,其中每

劍指offer搜索的後序遍歷序列

image 最大 樹的定義 結果 註意事項 ron com 題目 序列 一、題目: 輸入一個整數數組,判斷該數組是不是某二叉搜索樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的數組的任意兩個數字都互不相同。 二、思路: 1.搜索二叉樹

劍指offer——python第38題的深度

描述 sub pan 節點 solution class oot 返回值 self. 題目描述 輸入一棵二叉樹,求該樹的深度。從根結點到葉結點依次經過的結點(含根、葉結點)形成樹的一條路徑,最長路徑的長度為樹的深度。 解題思路 想了很久。。首先本渣渣就不太理解遞歸在pyt

資料結構——3.3 的遍歷及的同構

一、二叉樹的遍歷 1、先序遍歷 遍歷過程為: 1)訪問根結點 2)先序遍歷其左子樹 3)先序遍歷其右子樹 這樣的一種遍歷過程,其實也是一種遞迴的思想。 A(BDFE)(CGHI),先序遍歷=> ABDFECGHI 2、中序遍歷 遍歷過程為: 1)中序遍歷其左子樹

資料結構——3.2 及儲存結構

一、二叉樹的定義 二叉樹T:一個有窮的結點集合,這個集合可以為空;若不為空,則它是由根結點和稱為其左子樹TL和右子樹TR的兩個不相交的二叉樹組成。 1)二叉樹的五種基本形態 2)二叉樹的子樹有左右順序之分 3)特殊的二叉樹 斜二叉樹:只往一邊倒,只有左兒子,

資料結構——4.1 搜尋

一、二叉搜尋樹 一棵二叉樹,可以為空;如果不為空,滿足以下性質: 1)非空左子樹的所有鍵值小於其根結點的鍵值 2)非空右子樹的所有鍵值大於其根結點的鍵值 3)左右子樹都是二叉搜尋樹 二、二叉搜尋樹操作的特別函式 1、查詢 1)Find ① 查詢從根結點開始,如果樹