數據結構-AVL

AVL定義:

AVL的命名是由2個其發明者的名字組成的,G.M.Adelson-Velsky和E.M.Landis,兩個俄羅斯人。AVL既有平衡二叉樹的特性,又有二分搜索樹的特性。

平衡二叉樹:對于任意一個節點,左子樹和右子樹的高度差不能超過1。

平衡二叉樹

二分搜索樹:
1、二分搜索樹的每個節點的值,大于其左子樹的所有節點的值;小于其右子樹的所有節點的值;
2、每一顆子樹也是二分搜索樹。

AVL結構:

因為AVL是二分搜索樹,所以可以直接使用其結構,這里泛型使用了映射(Map),是為了符合更多的數據類型。然后新增了變量height,來表示當前節點的高度,其中葉子節點的高度為1。如下圖:

public class AVLTree<K extends Comparable<K>, V>{

    private class Node{
        //當前節點的值
        public K key;
        public V value;
        //左節點
        public Node left;
        //右節點
        public Node right;
        // 節點高度,葉子結點為1
        public int height;

        public Node(K key, V value){
            this.key = key;
            this.value = value;
            left = null;
            right = null;
            height = 1;
        }
    }

    private Node root;
    private int size;

獲取節點的高度和平衡因子:

平衡因子:左孩子的高度減去右孩子的高度。

1、獲取當前節點的高度:

// 獲取節點node的高度
private int getHeight(Node node){
        if (node == null) return 0;
        return node.height;
   }

2、獲取當前節點的平衡因子:

// 獲取節點node的平衡因子,平衡因子不在[-1,1]區間,說明不是平衡二叉樹
 private int getBalanceFactory(Node node){
        if (node == null) return 0;
        return getHeight(node.left) - getHeight(node.right);
   }

判斷該二叉樹是否是一顆二分搜索樹:

思路:因為二分搜索樹的中序遍歷會使元素自然排序,于是我們可以利用這個特性。對當前的二叉樹進行中序遍歷,把值存入一個集合List中,然后遍歷這個List,看前一個元素是否小于后一個元素,如果有一個不滿足返回false,直到全部元素遍歷完才返回true。

// 判斷該二叉樹是否是一顆二分搜索樹
    public boolean isBST(){
        // 二分搜索樹的一個特性:中序遍歷的結果是自然排序的
        ArrayList<K> keys = new ArrayList<>();
        inOrder(root, keys);
        for (int i = 1; i < keys.size(); i++) {
            if (keys.get(i - 1).compareTo(keys.get(i)) > 0)
                return false;
        }
        return true;
    }

    // 中序遍歷
    private void inOrder(Node node, ArrayList<K> keys) {
        if (node == null) return;

        inOrder(node.left, keys);
        keys.add(node.key);
        inOrder(node.right, keys);
    }

判斷該二叉樹是否是一顆平衡二叉樹:

思路:遍歷判斷當前節點的平衡因子balanceFactory是否 > 1,遞歸的終止條件為:1、執行到最后一個NULL元素,此時平衡因子為0,返回true;2、平衡因子>1,返回false。遞歸執行,只有左右孩子同時滿足平衡二叉樹的特性時才為true。

// 判斷該二叉樹是否是一顆平衡二叉樹,看平衡因子
    public boolean isBalanced(){
        return isBalanced(root);
    }

    // 判斷node為根的二叉樹是否是一顆平衡二叉樹,遞歸算法
    private boolean isBalanced(Node node) {
        if (node == null) return true;
        int balanceFactory = getBalanceFactory(node);
        if (Math.abs(balanceFactory) > 1)
            return false;

        // 左右子樹遞歸都為true才行
        return isBalanced(node.left) && isBalanced(node.right);
    }

LL與AVL的右旋轉:

在執行了添加元素或者刪除元素之后,由于會對節點高度進行修改,很可能破壞AVL的平衡性。于是需要左旋轉或者右旋轉操作來使AVL重新獲得平衡。
右旋轉的時機:插入的元素在不平衡節點的左側的左側(LL)。
我們先看右旋轉,如下圖:

右旋轉

首先從新增或者刪除的元素往上,找到第一個平衡因子=2的節點,如上圖的 y,然后取出 x 的右子樹 T3,然后使 y 的左子樹 x 的右孩子指向 y,然后讓 y 的左子樹指向 T3。此時 x 成為了新的AVL的根節點,并且新的AVL即符合平衡二叉樹的特性又符合二分搜索樹的特性。

右旋轉代碼實現:

    // 對節點y進行向右旋轉操作,返回旋轉后新的根節點x
    //        y                              x
    //       / \                           /   \
    //      x   T4     向右旋轉 (y)        z     y
    //     / \       - - - - - - - ->    / \   / \
    //    z   T3                       T1  T2 T3 T4
    //   / \
    // T1   T2
    private Node rightRotate(Node y){
        // 取出y 的左孩子x的右孩子T3
        Node x = y.left;
        Node T3 = x.right;
        x.right = y;
        y.left = T3;

        // 更新height
        y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
        x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
        return x;
    }

RR與AVL的左旋轉:

左旋轉和右旋轉剛好相反,于是實現機制是一樣的。
左旋轉的時機: 插入的元素在不平衡節點的右側的右側(RR)。

左旋轉之前

左旋轉之后

左旋轉的代碼實現:

    // 對節點y進行向左旋轉操作,返回旋轉后新的根節點x
    //    y                             x
    //  /  \                          /   \
    // T4   x      向左旋轉 (y)      y     z
    //     / \   - - - - - - - ->   / \   / \
    //   T3  z                     T4 T3 T1 T2
    //      / \
    //     T1 T2
    private Node leftRotate(Node y){
        // 取出y 的右孩子x的左孩子T3
        Node x = y.right;
        Node T3 = x.left;
        x.left = y;
        y.right = T3;

        // 更新height
        y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
        x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
        return x;
    }

LR:

插入的元素在不平衡節點的左側的右側(LR)。


LR

此時則需要讓不平衡節點 y 的左孩子 x 進行左旋轉,然后讓不平衡節點 y 的左子樹指向左旋轉之后的新的二叉樹的根節點 z。如下圖:


左旋轉之后

此時,已經儼然成為了LL的情況,于是 y 節點右旋轉就解決了啦。

RL:

插入的元素在不平衡節點的右側的左側(RL)。

RL

此時則需要讓不平衡節點 y 的右孩子 x 進行右旋轉,然后讓不平衡節點 y 的右子樹指向右旋轉之后的新的二叉樹的根節點 z。如下圖:


右旋轉之后

此時,已經儼然成為了RR的情況,于是 y 節點左旋轉就解決了啦。

添加元素:

遞歸操作,遞歸的終止條件:當前節點為NULL,表示到達最后一個根節點NULL,則將新節點放入該位置。然后比較所給的key與當前node節點的key的大小,如果小于,則新增的節點放在左子樹,于是往左子樹遞歸;反之,往右子樹遞歸;當二者相等時,則更新value值。然后更新node的height,計算node的平衡因子,如果平衡因子的絕對值 <= 1,則表示符合AVL特性,直接返回node;否則,就是已經破壞了AVL的平衡性,需要根據不同情況來進行左右旋轉。

    public void add(K key, V value) {
        root = add(root, key, value);
    }

    // 向 node 為根的二分搜索樹中添加新的元素(key, value)
    // 返回插入新節點后的二分搜索樹的根
    private Node add(Node node, K key, V value) {
        //遞歸終止條件
        if (node == null) {
            //表示到達最后一個根節點null,則將新節點放入該位置
            size++;
            return new Node(key, value);
        }
        if (key.compareTo(node.key) < 0){
            //遞歸步驟,往往遞歸終止條件的參數是和遞歸步驟的參數對應的
            //將插入新節點后的二分搜索樹的根掛在當前樹上
            node.left = add(node.left, key, value);
        }else if(key.compareTo(node.key) > 0){
            node.right = add(node.right, key, value);
        }else {
            // 更新value值
            node.value = value;
        }
        // 更新 height,左右孩子中大的height + 1
        node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
        // 計算平衡因子
        int balanceFactory = getBalanceFactory(node);
        if (Math.abs(balanceFactory) <= 1) return node;
        // 不符合平衡二叉樹條件,需要平衡維護
        if (balanceFactory > 1){
            if (getBalanceFactory(node.left) >= 0){
                // 插入的元素在不平衡節點的左側的左側(LL),采用右旋轉
                return rightRotate(node);
            }else{
                // 插入的元素在不平衡節點的左側的右側(LR),采用先左旋轉再右旋轉
                node.left = leftRotate(node.left);
                return rightRotate(node);
            }
        }
        if (balanceFactory < -1){
            if (getBalanceFactory(node.right) <= 0){
                // 插入的元素在不平衡節點的右側的右側(RR),采用左旋轉
                return leftRotate(node);
            }else {
                // 插入的元素在不平衡節點的右側的左側(RL),采用右旋轉再左旋轉
                node.right = rightRotate(node.right);
                return leftRotate(node);
            }
        }
        return node;
    }

刪除元素:

思路:依舊是遞歸執行,遞歸的終止條件:1、走到最后沒有找到 key;2、找到key,即node.key.compareTo(key) == 0,此時也要分情況考慮,當左子樹為空,則把右子樹的根節點作為新二叉樹的根節點,然后將node的右子樹置為NULL,讓GC回收;同理,右子樹為空時,則把左子樹的根節點作為新二叉樹的根節點,然后將node的左子樹置為NULL;當左右子樹都不為空時,則找出右子樹的最小值 successor 用來頂替待刪除節點的位置(后繼策略)。最后還是需要對上述操作之后返回的retNode進行平衡因子的判斷,如果失去了平衡性,則需要根據不同情況進行左右旋轉。

    public V remove(K key) {
        Node node = getNode(root, key);
        if (node != null) {
            root = remove(root, key);
            return node.value;
        }
        return null;
    }

    // 刪除以node為根的二分搜索樹中的鍵為key的節點
    // 返回刪除節點后的新的二分搜索樹
    private Node remove(Node node, K key) {
        // 遞歸終止條件: 1、走到最后沒有找到key ; 2、找到key
        if (node == null) return node;
        Node retNode;
        if (node.key.compareTo(key) == 0){
            if (node.left == null){
                // 待刪除的節點左子樹為空的情況
                Node rightNode = node.right;
                node.right = null;
                size--;
                retNode = rightNode;
            } else if (node.right == null){
                // 待刪除的節點右子樹為空的情況
                Node leftNode = node.left;
                node.left = null;
                size--;
                retNode = leftNode;
            }else {
                // 左右子樹都不為空,則找出右子樹的最小值,即采用e 的后繼。然后用這個節點頂替待刪除節點的位置
                Node successor = minimum(node.right);
                //由于removeMin 并未對刪除元素之后進行平衡檢查,所以要么加上,要不不用;
                // 我們這里采用不用,直接使用remove,因為successor就是node.right子樹的最小值
//            successor.right = removeMin(node.right);
                successor.right = remove(node.right, successor.key);
                successor.left = node.left;
                node.left = null;
                node.right = null;
                //注意:這里不需要size--,因為removeMin(node.right) 已經操作了size--
                retNode = successor;
            }
        }else if (key.compareTo(node.key) < 0){
            node.left = remove(node.left, key);
            retNode = node;
        }else {
            node.right = remove(node.right, key);
            retNode = node;
        }

        if (retNode == null) return retNode;
        // 更新 height,左右孩子中大的height + 1
        retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));
        // 計算平衡因子
        int balanceFactory = getBalanceFactory(retNode);
        // 不符合平衡二叉樹條件,需要平衡維護
        if (balanceFactory > 1){
            if (getBalanceFactory(retNode.left) >= 0){
                // 插入的元素在不平衡節點的左側的左側(LL),采用右旋轉
                return rightRotate(retNode);
            }else{
                // 插入的元素在不平衡節點的左側的右側(LR),采用先左旋轉再右旋轉
                retNode.left = leftRotate(retNode.left);
                return rightRotate(retNode);
            }
        }
        if (balanceFactory < -1){
            if (getBalanceFactory(retNode.right) <= 0){
                // 插入的元素在不平衡節點的右側的右側(RR),采用左旋轉
                return leftRotate(retNode);
            }else {
                // 插入的元素在不平衡節點的右側的左側(RL),采用右旋轉再左旋轉
                retNode.right = rightRotate(retNode.right);
                return leftRotate(retNode);
            }
        }
        return retNode;
    }

小結:

至此,AVL的基本操作已經介紹完了,由于它不會像二分搜索樹那樣退化成鏈表,所以它的添加元素和刪除元素的時間復雜度都是O(log n)。

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

推薦閱讀更多精彩內容

  • 1 序 2016年6月25日夜,帝都,天下著大雨,拖著行李箱和同學在校門口照了最后一張合照,搬離寢室打車去了提前租...
    RichardJieChen閱讀 5,125評論 0 12
  • 這篇文章收錄在我的 Github 上 algorithms-tutorial,另外記錄了些算法題解,感興趣的可以看...
    Lindz閱讀 2,509評論 3 11
  • 基于樹實現的數據結構,具有兩個核心特征: 邏輯結構:數據元素之間具有層次關系; 數據運算:操作方法具有Log級的平...
    yhthu閱讀 4,300評論 1 5
  • 在小欖車站等候9:40到三水的班車,意味著2017年的暑假接近尾聲了。這個暑假過得比以前無所事事充實點。 ...
    庸人別自擾閱讀 349評論 0 2
  • 你是怎樣的,你就會看到怎樣的.
    徐一朵兒閱讀 92評論 0 0