1. 程式人生 > >平衡二叉樹和AVL

平衡二叉樹和AVL

1  概述      

        對於一棵二分搜尋樹,如果我們的資料是順序新增到二分搜尋樹中,它就會退化成一個連結串列。我們如何解決這個問題呢,我們需要在現有的二分搜尋樹的基礎上新增一些機制,使得我們二分搜尋樹能維持平衡二叉樹這樣的一個性質,而AVL就是一種最為經典的平衡二叉樹。

什麼樣的樹才是平衡二叉樹呢?

滿二叉數:除了葉子節點,其餘節點都有左右子樹,所以是一棵平衡二叉樹

而最大堆中所說的完全二叉樹:因為它葉子節點中最大深度值與最小深度值相差不超過1,也是平衡二叉樹。、

我們在AVL中所維護的這種平衡二叉樹,它的條件更加寬鬆些:對於任意一個節點,左子樹和右子樹的高度差不能超過1

現在我們按照二分搜尋樹新增元素的方法往上圖中的樹中新增2和7兩個元素,可得下圖

我們再來計算各個節點的高度和平衡因子

可以看到,根節點12和它的左孩子節點8的平衡因子都為2,所以此時就不是平衡二叉樹了。

2. 新增節點(4種情況)

       我們要注意,AVL樹只是對二分搜尋樹的改進,它本質上還是一個二分搜尋樹,所以我們只需要在二分搜尋樹程式碼的基礎上進行修改就可以了。二分搜尋樹的程式碼在我github上:GitHub連結

      由於我們只有新新增一個節點,才會導致以前的平衡二叉樹可能變為不平衡,而不平衡節點的出現只可能出現在插入節點(葉子節點)的父輩節點上。所以我們維護平衡的時機應該是我們加入節點之後,我們沿著這個節點向上回溯來維持這個平衡性,由於我們新增節點的操作時遞迴進行的,所以我們來維護也是很容易的。

維護時的情況有四種,LL,RR,LR,RL

1)  LL——右旋轉(插入的元素在不平衡節點的左側的左側)

我們來看個例子:

上面這個圖是個平衡二叉樹,現在往這個平衡二叉樹新增一個節點2:

       可以看到,此時節點8左子樹高度為3,右子樹高度為1,所以節點8的平衡因子為2,所以節點8打破了平衡二叉樹的性質,所以我們需要對節點8這個位置進行一個平衡的維護,那麼我們怎麼來維護呢?

      上面的情況我們可以抽象成下圖中的左邊,我們分兩步,第一步:將y連線在x節點的右孩子上;第二部:將T3連線在y的左孩子上;這樣就變成下圖右邊的樣子了,這樣既滿足了二分搜尋樹的性質,又滿足了平衡二叉樹的性質

2)  RR——左旋轉(插入的元素在不平衡節點的右側的右側)

上面兩種情況的程式碼:

    // 向二分搜尋樹中新增新的元素(key, value)
    public void add(K key, V value){
        root = add(root, key, value);
    }
    private Node add(Node node, K key, V value){

        if(node == 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 // key.compareTo(node.key) == 0
            node.value = value;

        // 更新height
        node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));

        // 計算平衡因子
        int balanceFactor = getBalanceFactor(node);

        // 平衡維護
        if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)
            return rightRotate(node);

        if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)
            return leftRotate(node);

        return node;
    }
    // 對節點y進行向右旋轉操作,返回旋轉後新的根節點x
    //        y                              x
    //       / \                           /   \
    //      x   T4     向右旋轉 (y)        z     y
    //     / \       - - - - - - - ->    / \   / \
    //    z   T3                       T1  T2 T3 T4
    //   / \
    // T1   T2
    private Node rightRotate(Node y) {
        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;
    }
    // 對節點y進行向左旋轉操作,返回旋轉後新的根節點x
    //    y                             x
    //  /  \                          /   \
    // T1   x      向左旋轉 (y)       y     z
    //     / \   - - - - - - - ->   / \   / \
    //   T2  z                     T1 T2 T3 T4
    //      / \
    //     T3 T4
    private Node leftRotate(Node y) {
        Node x = y.right;
        Node T2 = x.left;

        // 向左旋轉過程
        x.left = y;
        y.right = T2;

        // 更新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;
    }

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

這種情況如下圖:

我們看下上圖的左邊,如果我們現在使用右旋轉,則12變成8的左孩子,10變成8的右孩子,違反了二分搜尋樹的原則。所以我們將上面的情況抽象:

此時,我們先對x進行左旋轉

可以看到,此時已經轉換成LL的情況了,然後我們再按第一節右旋轉的方法對y節點進行右旋轉就可以啦

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

先對x進行右旋轉

然後再對y進行左旋轉就可以了

對於程式碼方面,第三四種情況直接服用左旋轉和右旋轉的程式碼就行啦

注意綠色的條件要搞清楚

3.   刪除節點

原理和新增節點一樣,直接看程式碼吧:

    // 從二分搜尋樹中刪除鍵為key的節點
    public V remove(K key){

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

    private Node remove(Node node, K key){

        if( node == null )
            return null;

        Node retNode;
        if( key.compareTo(node.key) < 0 ){
            node.left = remove(node.left , key);
            // return node;
            retNode = node;
        }
        else if(key.compareTo(node.key) > 0 ){
            node.right = remove(node.right, key);
            // return node;
            retNode = node;
        }
        else{   // key.compareTo(node.key) == 0

            // 待刪除節點左子樹為空的情況
            if(node.left == null){
                Node rightNode = node.right;
                node.right = null;
                size --;
                // return rightNode;
                retNode = rightNode;
            }

            // 待刪除節點右子樹為空的情況
            else if(node.right == null){
                Node leftNode = node.left;
                node.left = null;
                size --;
                // return leftNode;
                retNode = leftNode;
            }

            // 待刪除節點左右子樹均不為空的情況
            else{
                // 找到比待刪除節點大的最小節點, 即待刪除節點右子樹的最小節點
                // 用這個節點頂替待刪除節點的位置
                Node successor = minimum(node.right);
                //successor.right = removeMin(node.right);
                successor.right = remove(node.right, successor.key);
                successor.left = node.left;

                node.left = node.right = null;

                // return successor;
                retNode = successor;
            }
        }

        if(retNode == null)
            return null;

        // 更新height
        retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));

        // 計算平衡因子
        int balanceFactor = getBalanceFactor(retNode);

        // 平衡維護
        // LL
        if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0)
            return rightRotate(retNode);

        // RR
        if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0)
            return leftRotate(retNode);

        // LR
        if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
            retNode.left = leftRotate(retNode.left);
            return rightRotate(retNode);
        }

        // RL
        if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0) {
            retNode.right = rightRotate(retNode.right);
            return leftRotate(retNode);
        }

        return retNode;
    }