數據結構-平衡二叉樹

定義

平衡二叉樹,是對二叉搜索樹的一種優化。

向二叉搜索樹中插入元素時,不同的插入次序,將構造出不同結構的樹。通俗來講,就是會導致樹的深度平均查找長度(ASL averge search length)不同;以下圖為例。

bst_tree_insert

明顯可以看出,中間(b)這種結構是比較好的。整個二叉搜索樹左右兩邊顯得比較平均,不像最后一種完全成了一顆右斜樹,或者說是單向鏈表,同時也可以看到其ASL=3.0 是這三種結構中最小的。

總的來說,目的就是想讓整個二叉搜索樹變得比較矮胖,而不是高瘦,或者是一邊倒的傾斜。因為,矮胖意味著樹比較低,使得查找某個元素的能更快速。

平衡因子:Balance Factor,簡稱BF,BF(T)=hL-hR. 其中L和hR分別為二叉樹左右子樹的高度。

平衡二叉樹:(AVL 樹):空樹,或者任一結點左、右子樹高度差的絕對值不超過1. 即|BF(T)|<=1.

avl_tree

圖中結點3和5的平衡因子均為2,因此不是平衡二叉樹。最后一棵樹,根節點7平衡因子為2,同樣不是平衡二叉樹。

對于給定結點數為n的AVL樹,最大高度為O(log2n).

也就說,從n個數中,查找一個特定值時,最多需要log2n次。因此,AVL 是一種特別適合進行查找操作的樹。

平衡二叉樹的調整

四種失衡的情況

在平衡二叉樹中,當我們插入新的元素時,為了保證二叉搜索樹的特性,很容易導致某些結點失衡,即該結點的平衡因子大于1。

而在二叉樹中,任意結點孩子最多只有左右兩個,而且導致失去平衡的必要條件就是當前結點的兩顆子樹的高度差等于2。因此,致使一個結點失衡的插入操作有以下4中。

  • 在結點的左子樹的左子樹插入元素,LL 插入;
  • 在結點的左子樹的右子樹插入元素,LR 插入;
  • 在結點的右子樹的左子樹插入元素,RL 插入;
  • 在結點的右子樹的右子樹插入元素,RR 插入。
失去平衡的二叉搜索樹
  • LL(一)中結點1的插入導致結點8失衡,而插入的位置是在其左子樹的左子樹上,同樣LL(二)中,結點3插入同樣導致結點8失衡,這里需要注意子樹是從受影響的結點算起,雖然3插在了右邊,但他依舊是在8(失衡結點)左子樹的左子樹上,因此屬于LL 插入。
  • LR(一),結點5插入導致結點8失衡,插入位置是在其左子樹的右子樹上,同樣LR(二)結點7的插入也是同理,因此這二者都屬于LR 插入。

后面兩種失衡現象可以當做是前兩者的鏡像,原理都是一樣的。

對四種失衡情況的調整策略

面對以上4種失衡的情況,在AVL 樹中將采用LL(左左),LR(左右),RR(右右)和RL(右左) 四種旋轉方式進行調整。

1. LL(左左)旋轉

ll

如圖,這種情況下BL結點插入元素導致結點A失衡,因此我們的操作就是圍繞失衡的結點(圖中A)和導致其失衡的結點(圖中B)進行。具體來說就是圍繞結點B 將樹順時針(左手)旋轉,最終結果就是B成為了根節點(相對而言),A 變成了B的右子樹,而B原來的右子樹(BR)變成了A 的左子樹。

看一下代碼實現:

    /**
     * 左旋
     *
     * @param node 失衡結點 
     * @return 旋轉后根節點
     */
    private TreeNode<T> leftRotate(TreeNode<T> node) {
        // 將失衡結點的左子樹賦給一個臨時結點,也就是將A的左子樹B 賦給新的結點
        TreeNode<T> newRoot = node.leftChild;
        // 將B 被右子樹BR 掛在A 的左子樹上
        node.leftChild = newRoot.rightChild;
        // B 的右子樹為失衡的結點即A
        newRoot.rightChild = node;
        // 結點A 的高度為左右子樹高度最大值加1
        node.height = getMax(height(node.leftChild), height(node.rightChild)) + 1;
        // 結點B 的高度為左右子樹高度最大值加1
        newRoot.height = getMax(height(newRoot.leftChild), newRoot.height) + 1;
        // 返回根節點
        return newRoot;
    }

結合注釋應該很好理解。

2. RR(右右)旋轉

rr

理解了LL,RR就是同理了,圍繞的同樣是導致失衡的結點B,只不過旋轉方向變成了逆時針(右手向內)。

    /**
     * 右旋
     *
     * @param node
     * @return
     */
    private TreeNode<T> rightRotate(TreeNode<T> node) {
        TreeNode<T> newRoot = node.rightChild;
        node.rightChild = newRoot.leftChild;
        newRoot.leftChild = node;

        node.height = getMax(height(node.leftChild), height(node.rightChild)) + 1;
        newRoot.height = getMax(height(newRoot.rightChild), node.height) + 1;

        return newRoot;
    }

3. LR(右左)旋轉

lr

看上面的圖可能有點暈,這里看以具體的例子。

rl_example

上圖中Jan結點的插入導致May結點失衡,而Jan結點又處在May結點左子樹的右子樹上,是LR 插入導致的失衡,面對這種情況我們可以進行LR旋轉,需要關注的三個結點是May,Aug和Mar。具體來說,LR 旋轉可以分解為RR旋轉和LL旋轉。首先圍繞Aug和Mar,進行一次RR旋轉,然后圍繞Mar和May在進行一次LL旋轉。這樣最終就完成了LR 旋轉,最終的結果是樹仍然為AVL樹。

    /**
     * LR 左右旋轉
     *
     * @param node
     * @return
     */
    private TreeNode<T> leftRightRotate(TreeNode<T> node) {
        // 首先圍繞失衡結點的左子樹(圖中Aug) 和Mar進行一次右旋,這樣Mar 和 Aug 換了位置
        node.leftChild = rightRotate(node.leftChild);
        // 最后,圍繞May和Mar進行一次左旋
        return leftRotate(node);
    }

4. RL(左右)旋轉

rl

前面說過了,RL其實就是LR 的鏡像,因此這里道理都是一樣的,只過順序顛倒而已。

/**
     * RL 右左旋轉
     *
     * @param node
     * @return
     */
    private TreeNode<T> rightLeftRotate(TreeNode<T> node) {
        node.rightChild = leftRotate(node.rightChild);
        return rightRotate(node);
    }

AVL 樹的實現

以上就是AVL 樹所有的理論基礎,下面看看如何去實現。

  • 結點的定義

AVL 樹首先是二叉搜索樹,因此它的結點也必須是可比較。同時為了方便,會加入一個表示當前結點高度的height字段。

/**
 * Created by engineer on 2017/10/31.
 *
 * AVL 樹節點定義
 */

public class TreeNode<T extends Comparable<T>> {

    // 數據域
    private T data;
    // 左子樹
    public TreeNode<T> leftChild;
    // 右子樹
    public TreeNode<T> rightChild;

    //當前結點的高度
    public int height;


    public TreeNode(T data) {
        this(null, data, null);
    }

    public TreeNode(TreeNode leftChild, T data, TreeNode rightChild) {
        this(data, leftChild, rightChild, 0);
    }

    public TreeNode(T data, TreeNode<T> leftChild, TreeNode<T> rightChild, int height) {
        this.data = data;
        this.leftChild = leftChild;
        this.rightChild = rightChild;
        this.height = height;
    }

    public T getData() {
        return data;
    }

    public TreeNode<T> getLeftChild() {
        return leftChild;
    }

    public TreeNode<T> getRightChild() {
        return rightChild;
    }

    public void setData(T data) {
        this.data = data;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }
}
  • 平衡二叉樹插入
/**
     * 插入結點
     *
     * @param value
     */
    public void insert(T value) {
        root = insert(root, value);
    }

    private TreeNode<T> insert(TreeNode<T> node, T value) {
        if (node == null) {
            // 新建節點
            node = new TreeNode<T>(value);
            if (node == null) {
                return null;
            }
        } else {
            int cmp = value.compareTo(node.getData());

            if (cmp < 0) {    // 應該將value插入到"node的左子樹"的情況
                node.leftChild = insert(node.leftChild, value);
                // 插入節點后,若AVL樹失去平衡,則進行相應的調節。
                if (height(node.leftChild) - height(node.rightChild) == 2) {
                    if (value.compareTo(node.leftChild.getData()) < 0)
                        node = leftRotate(node);
                    else
                        node = leftRightRotate(node);
                }
            } else if (cmp > 0) {    // 應該將value插入到"node的右子樹"的情況
                node.rightChild = insert(node.rightChild, value);
                // 插入節點后,若AVL樹失去平衡,則進行相應的調節。
                if (height(node.rightChild) - height(node.leftChild) == 2) {
                    if (value.compareTo(node.rightChild.getData()) > 0)
                        node = rightRotate(node);
                    else
                        node = rightLeftRotate(node);
                }
            } else {    // cmp==0
                System.out.println("添加失敗:不允許添加相同的節點!");
            }
        }

        node.height = getMax(height(node.leftChild), height(node.rightChild)) + 1;

        return node;
    }

測試平衡二叉樹

public class AvlTreeTest {

    private static Integer[] arrays = new Integer[]{10, 8, 3, 12, 9, 4, 5, 7, 1, 11, 17};

    public static void main(String[] args) {
        AvlTree<Integer> mAvlTree = new AvlTree<>();
        for (int i = 0; i < arrays.length; i++) {
            mAvlTree.insert(arrays[i]);
        }

        mAvlTree.printTree();
    }
}

這里我們測試平衡二叉樹采用和上一節二叉搜索樹中同樣的數據。首先看一下樹遍歷打印結果:

前序遍歷:8 4 3 1 5 7 10 9 12 11 17 
中序遍歷:1 3 4 5 7 8 9 10 11 12 17 
后序遍歷:1 3 7 5 4 9 11 17 12 10 8 

這樣的遍歷的結構,相對應的平衡二叉樹將是如下:

平衡二叉樹

在和上一節構造的二叉樹對比一下:

二叉搜索樹

很明顯,平衡二叉樹是一種更加友好的搜索樹,在平衡二叉樹中查找7這個元素,最大比較4次,而在普通的二叉搜索樹中需要找6次。總體來說,平衡二叉樹結合平衡因子構造出了一顆十分便于查找的二叉搜索樹。


好了,平衡二叉樹就到這里了。


參考文檔

AVL樹(三)之 Java的實現

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容