AVL 平衡二叉搜索樹 支持鍵值 簡介+實現

爲什麼要平衡

一般的搜索樹, 如果元素是順序加入的話, 那麼這棵樹就會退化成鏈表

什麼是平衡

對於任意一個節點, 左右子樹的高度差不超過1的樹就是平衡二叉樹, 比如下面這棵樹
height表示高度, 父節點的高度是左右子節點中最大的那個高度加一
平衡二叉樹的高度和節點數之間的關係是 O(log n)的
在這裏插入圖片描述
因爲每次加入新的節點, 都要看節點的高度, 所以節點的結構如下

class Node{
    key;  // 用來統計詞頻的話, key就是單詞
    value;  // value是單詞的出現頻率
    Node left, right;  // 左右節點
    height;  // 樹的高度
}

平衡因子

父節點的平衡因子(balance factor)是左子樹與右子樹的高度(height)之差(可正可負)
葉子節點的左右子節點爲空, 高度爲0, 平衡因子爲0
當樹中有一個節點平衡因子大於1或小於-1時, 這棵樹就不平衡, 如下圖
在這裏插入圖片描述

不平衡的情況和平衡的方法

只有一個節點, 或者兩個節點的樹是平衡的
當向一棵平衡樹插入一個新的節點時, 可能會不平衡
如果不平衡, 那麼那個新插入的節點的父親節點, 祖先節點的平衡因子都會大於1或小於-1

所以新插入節點都要向上回溯來維護其平衡性

不平衡的情況有4種:

LL

如下圖中, 是框中的節點帶來了不平衡
在這裏插入圖片描述
我們可以把框中部分用下圖來代替, 藍色部分是他們的子樹
這種情況可以稱爲 Left Left, 意思是新添了一個節點 z 到根節點 y 左子樹的左子樹, 導致根節點的平衡因子絕對值大於1

這種情況用代碼表示爲getBalanceFactor(y) > 1 && getBalanceFactor(x) > 0, 再次說明 平衡因子是拿左子樹的height減右子樹的height, getBalanceFactor(y) = 2, getBalanceFactor(x) = 1
在這裏插入圖片描述

只需要把x的右子樹換成y, y的左子樹換位x原來的右子樹 T3 即可達到平衡, 同時還保持了二叉樹搜索樹的性質
相當於z不動, 將y節點以x爲軸順時針旋轉, 也稱爲右旋轉, 效果如下
在這裏插入圖片描述

Node rightRotate(Node y){
   Node x = y.left;
   Node T3 = x.left;

   // 向右旋轉
    x.right = y;
    y.left = T3;

    // 更新height, 先更新y, 再更新x, 因爲y在下面
    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

還有一種情況我們稱爲 Right Right, 意思是新添了一個節點 z 到根節點 y 右子樹的右子樹

這種情況用代碼表示爲getBalanceFactor(y) < -1 && getBalanceFactor(x) < 0
在這裏插入圖片描述
只需要把x的左子樹換成y, y的右子樹換位x原來的左子樹 T3 即可達到平衡, 同時還保持了二叉樹搜索樹的性質
相當於z不動, 將y節點以x爲軸逆時針旋轉, 也稱爲左旋轉, 效果如下
在這裏插入圖片描述

Node leftRotate(Node y){
    Node x = y.right;
    Node T3 = x.left;

    // 向左旋轉
    x.left = y;
    y.right = T3;

    // 更新height, 先更新y, 再更新x, 因爲y在下面
    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

另一種情況稱爲 Left Right, 意思是新添了一個節點 z 到根節點 y 左子樹的右子樹

這種情況用代碼表示爲getBalanceFactor(y) > 1 && getBalanceFactor(x) < 0
在這裏插入圖片描述
這種情況需要兩次旋轉 (要注意得保持二叉搜索樹的性質),
首先把節點x以節點z爲軸逆時針旋轉, 節點y不動 (左旋轉)
再把節點y以z爲軸順時針旋轉, 得到效果如下 (右旋轉)
在這裏插入圖片描述

RL

另一種情況稱爲 Right Left, 意思是新添了一個節點 z 到根節點 y 右子樹的左子樹

這種情況用代碼表示爲getBalanceFactor(y) < -1 && getBalanceFactor(x) > 0
在這裏插入圖片描述
這種情況也需要兩次旋轉
先節點x繞節點z順時針旋轉 (右旋轉)
節點y再繞z逆時針旋轉 (左旋轉)
在這裏插入圖片描述

刪除操作

刪除一個元素的話, 就得拿一個新的元素來代替自己放在原來的位置, 我們可以拿比它大的最小元素, 或者比它小的最大元素來代替它, 這樣可以同時保持二叉搜索樹的性質

待刪除的節點分爲四種情況

  1. 待刪除節點左子樹爲空
    可以拿它的右子樹代替掉自己 (用它大的最小元素代替)
  2. 待刪除節點右子樹爲空
    可以拿它的左子樹代替掉自己 (用比它小的最大元素代替)
  3. 待刪除節點左右子樹爲空
    葉子節點也可以看成是有null作爲孩子, 這樣就可以用上面的方法處理掉
  4. 待刪除節點左右子樹不爲空
    如果用 比它大的最小元素, 那就是拿它右子樹的最小元素來代替它
    如果用 比它小的最大元素, 那就是拿它左子樹的最大元素來代替它

刪除之後記得要重新維護平衡, 方法同上

實現

Map.java

public interface Map<K, V> {
    void add(K key, V value);
    V remove(K key);
    boolean contains(K key);
    V get(K key);
    void set(K key, V newValue);
    int getSize();
    boolean isEmpty();
}

AVL.tree

import test.FileOperation;  // 測試用, 可刪

import java.util.ArrayList;  // 測試用, 可刪

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

    private class Node{
        public K key;
        public V value;
        public Node left, right;
        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;

    public AVLTree(){
        root = null;
        size = 0;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 判斷該二叉樹是否是一棵二分搜索樹
     * @return
     */
    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;
    }

    /**
     * 判斷一棵樹是否是平衡二叉樹
     * @return
     */
    public boolean isBalanced(){
        return isBalanced(root);
    }

    /**
     * 判斷以Node爲根的二叉樹是否是一棵平衡二叉樹,遞歸算法
     * @param node
     * @return
     */
    private boolean isBalanced(Node node) {
        if(node == null)
            return true;

        int balanceFactor = getBalanceFactor(node);
        if(Math.abs(balanceFactor) > 1)
            return false;
        return isBalanced(node.left) && isBalanced(node.right);  // 父節點的平衡因子小於1, 子節點不一定也都小於1
    }

    /**
     * 中序遍歷, 保存到一個ArrayList中
     * @param node
     * @param keys
     */
    private void inOrder(Node node, ArrayList<K> keys) {
        if(node == null){
            return;
        }

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


    private int getHeight(Node node){
        if(node == null)
            return 0;
        return node.height;
    }

    /**
     * 向二分搜索書中添加元素(key, value)
     * @param key
     * @param value
     */
    @Override
    public void add(K key, V value) {
        root = add(root, key, value);
    }

    /**
     * 獲得節點node的平衡因子
     * @param node
     * @return
     */
    private int getBalanceFactor(Node node){
        if(node == null)
            return 0;

        return getHeight(node.left) - getHeight(node.right);
    }

    // 對節點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, 再更新x, 因爲y在下面
        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
    //  /  \                          /   \
    // T4   x      向左旋轉 (y)       y     z
    //     / \   - - - - - - - ->   / \   / \
    //   T3  z                     T4 T3 T1 T2
    //      / \
    //     T1 T2
    private Node leftRotate(Node y){
        Node x = y.right;
        Node T3 = x.left;

        // 向左旋轉
        x.left = y;
        y.right = T3;

        // 更新height, 先更新y, 再更新x, 因爲y在下面
        y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
        x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;

        return x;
    }


    /**
     * 向以node爲根的二分搜索樹中插入元素(key, value),遞歸算法
     * 如果key已存在, 則更新value
     * @param node
     * @param key
     * @param value
     * @return 插入新節點後二分搜索樹的根
     */
    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
            node.value = value;

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

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

        // 3. 維護平衡
        // LL
        if(balanceFactor > 1 && getBalanceFactor(node.left) > 0)  // 不用>=0, 因爲node.left左右一定是不相等的, 如果相等的話, 那就是有兩個節點導致了不平衡, 這是不可能的, 但是加上也可以啦
            return rightRotate(node);

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

        // LR
        if(balanceFactor > 1 && getBalanceFactor(node.left) < 0){
            node.left = leftRotate(node.left);  // 轉化成LL的情況
            return rightRotate(node);
        }

        // RL
        if(balanceFactor < -1 && getBalanceFactor(node.right) > 0){
            node.right = rightRotate(node.right);  // 轉化成RR的情況
            return leftRotate(node);
        }
        // 平衡結束

        return node;
    }

    /**
     *
     * @return 返回以node爲根的二分搜索樹的最小值所在的節點
     */
    private Node minimum(Node node){
        if(node.left == null)
            return node;

        return minimum(node.left);
    }

    /**
     * 從二分搜索樹中刪除鍵爲key的節點
     * @param key
     * @return
     */
    @Override
    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);
            retNode = node;  // 刪除完節點後可能會打破平衡, 先不返回
        }
        else if(key.compareTo(node.key) > 0){
            node.right = remove(node.right, key);
            retNode = node;
        }
        else{ // key.compareTo(node.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{
                // 找到比待刪除節點大的最小節點, 即待刪除節點右子樹的最小節點
                // 用這個節點頂替待刪除節點的位置
                Node successor = minimum(node.right);
                successor.right = remove(node.right, successor.key);
                successor.left = node.left;

                node.left = null;
                node.right = null;
                // size--; removeMin已經改過size了
                retNode = successor;
            }
        }
        if(retNode == null){  // retNode爲空
            return null;
        }

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

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

        // 3. 維護平衡
        // LL
        // getBalanceFactor(retNode.left) >= 0 的等於號是必須要的, 如果刪除T4的話,
        //        y     就會出現z, T3兩個節點同時導致不平衡, 這在添加的時候不可能出現,
        //       / \    但在刪除的時候可能出現
        //      x   T4
        //     / \
        //    z   T3
        if(balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0)
            return rightRotate(retNode);

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

        // LR
        // getBalanceFactor(retNode.left) >= 0 的等於號何以省略, 因爲 =0 就是上面的情況, 在上面用比較簡單的方法就處理掉了
        if(balanceFactor > 1 && getBalanceFactor(retNode.left) < 0){
            retNode.left = leftRotate(retNode.left);  // 轉化成LL的情況
            return rightRotate(retNode);
        }

        // RL
        if(balanceFactor < -1 && getBalanceFactor(retNode.right) > 0){
            retNode.right = rightRotate(retNode.right);  // 轉化成RR的情況
            return leftRotate(retNode);
        }
        // 平衡結束

        return retNode;
    }

    /**
     * @return 以node爲根節點的二分搜索樹中,key所在的節點
     */
    private Node getNode(Node node, K key){
        if(node == null){
            return null;
        }

        if(key.equals(node.key))
            return node;
        else if(key.compareTo(node.key) < 0)
            return getNode(node.left, key);
        else // key.compareTo(node.key) > 0
            return getNode(node.right, key);
    }

    @Override
    public boolean contains(K key) {
        return getNode(root, key) != null;
    }

    @Override
    public V get(K key) {
        Node node = getNode(root, key);
        return node == null ? null : node.value;
    }

    @Override
    public void set(K key, V newValue) {
        Node node = getNode(root, key);
        if(node == null)
            throw new IllegalArgumentException(key + " doesn't exist!");

        node.value = newValue;
    }

	// 僅測試用
    public static void main(String[] args) {
        System.out.println("Pride and Prejudice");

        ArrayList<String> words = new ArrayList<>();
        if(FileOperation.readFile("on-the-road.txt", words)) {
            System.out.println("Total words: " + words.size());

            AVLTree<String, Integer> map = new AVLTree<>();
            for (String word : words) {
                if (map.contains(word))
                    map.set(word, map.get(word) + 1);
                else
                    map.add(word, 1);
            }

            System.out.println("Total different words: " + map.getSize());
            System.out.println("Frequency of PRIDE: " + map.get("pride"));
            System.out.println("Frequency of PREJUDICE: " + map.get("prejudice"));

            System.out.println("is BST: " + map.isBST());
            System.out.println("is Balanced: " + map.isBalanced());

            for(String word : words){
                map.remove(word);
                if(!map.isBST() || !map.isBalanced()){
                    throw new RuntimeException("ERROR");
                }
            }
        }

        System.out.println();
    }
}

把ALVTree包裝一下就作爲AVLMap, 如果不使用value, 只用key的話也可以作爲AVLSet使用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章