二叉平衡樹 AVL

二叉查找樹 BST : https://blog.csdn.net/cj_286/article/details/90183298

二叉平衡樹 AVL : https://blog.csdn.net/cj_286/article/details/90217072

紅黑樹 RBT : https://blog.csdn.net/cj_286/article/details/90245150

 

爲什麼需要AVL樹

BST與TreeMap的效率對比

1.隨機序列的存取 (他們的存取速度差不多)
2.升序或降序序列的存取 (兩萬的數據量TreeMap的存取(先存後取)速度是BST的四百倍左右)
 

                          BST        TreeMap
      隨機序列             OK           OK
    升序或降序序列         Slow          OK

爲什麼BST在極端情況下存取速度會如此的慢呢,因爲在極端情況下,BST會退化爲鏈表(升序或降序),時間複雜度會由原來的O(logN)退化爲O(N),所以查詢速度會變慢


        4                        1                                     7
     /    \                       \                                   /
    2      6     O(logN)-->        2            O(N)-->              6       O(N)
  /  \   /  \                       \                               /
 1    3  5   7                       3                             5
                                      \                           /	
                                       4                         4
                                        \                       /
                                         5                     3
                                          \                   /
                                           6                 2
                                            \               /
                                             7             1

   BST隨機存儲                    BST升序存儲                     BST降序存儲

在升序或降序的情況下BST明顯是滿足不了需求的,那麼有沒有哪種數據結構,對於任何插入節點或者刪除節點的操作都能自動的保持樹的平衡,這時AVL樹就誕生了,AVL樹它是一種自平衡的樹。

性質

以下AVL的代碼是基於BST代碼的,只是添加了使其平衡的代碼

在計算機科學中,AVL樹是最先發明的自平衡二叉查找樹。在AVL樹中任何節點的兩個子樹的高度最大差別爲1,所以它也被稱爲高度平衡樹。增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。AVL樹得名於它的發明者G. M. Adelson-Velsky和E. M. Landis

AVL樹本質上還是一棵二叉搜索樹,它的特點是:
1.本身首先是一棵二叉搜索樹。
2.帶有平衡條件:每個結點的左右子樹的高度之差的絕對值(平衡因子Balance Factor)最多爲1。
3.空樹、左右子樹都是AVL
也就是說,AVL樹,本質上是帶了平衡功能的二叉查找樹(二叉排序樹,二叉搜索樹)。

對比AVL樹和非AVL樹

avl-1-1-1

由圖可知,一棵AVL樹不一定是完全二叉樹,AVL樹它的每個子節點的平衡因子的絕對值都是小於等於1的,它的每個子節點都是一個AVL樹

非AVL樹轉爲AVL樹
爲了簡化操作,只考慮三個節點的情況
三個節點單旋轉

avl-1-2-1

以3爲根節點順時針旋轉,旋轉之後,原來的根節點3變成了原來的左子樹2的右子樹,原來的左子樹2變成了根節點,這時二叉樹就恢復平衡變成AVL樹
三個節點雙旋轉

avl-1-2-2

首先先處理節點1,將節點1進行左旋轉,原來的右子樹2變成了新的根節點,原來的1變成了2的左子樹,這時的情況和上面的單旋轉情況一樣了,以3節點右旋就變成了一個AVL樹了

JDK TreeMap右旋源碼解析

avl-1-3-1

紅色節點是相對位置發生了改變,l原本是左子樹,右旋過後,l代替了p,p變成了l的右子樹,原本l.right是l的右孩子,右旋之後,l.right變成了p的左孩子。

private void rotateRight(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> l = p.left; //取得p的左孩子l
            p.left = l.right; //l的右孩子l.right變成p的左孩子
            if (l.right != null) l.right.parent = p; //l.right的父節點設置爲p
            l.parent = p.parent; //l的父節點設置爲p的父節點p.parent
            if (p.parent == null)
                root = l;
            else if (p.parent.right == p) 
                p.parent.right = l; //p.parent的左孩子或者右孩子設置爲l
            else p.parent.left = l;
            l.right = p; //l的右子樹設置爲p
            p.parent = l;//p的父節點設置爲l
        }
    }

JDK TreeMap左旋源碼解析

avl-1-3-2

紅色節點是相對位置發生了改變,r原本是右子樹,左旋過後,r替代了p,p變成了r的左孩子,原本的r.left是r的左孩子,左旋之後,r.left變成了p的右孩子
左旋和右旋完全對稱

private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> r = p.right;
            p.right = r.left;
            if (r.left != null)
                r.left.parent = p;
            r.parent = p.parent;
            if (p.parent == null)
                root = r;
            else if (p.parent.left == p)
                p.parent.left = r;
            else
                p.parent.right = r;
            r.left = p;
            p.parent = r;
        }
    }

什麼時候需要旋轉
1,插入關鍵字key後,結點p的平衡因子由原來 的1或者-1,變成了2或者-2,則需要旋轉:值考慮插入key到左子樹left的情況,即平衡因子是2
    情況1:key < left.key,即插入到left的左子樹,需要進行單旋轉,將結點p右旋 (圖:avl-1-4-1)
    情況2:key > left.key,即插入到left的右子樹,需要進行雙旋轉,先將left左旋,再將p右旋 (圖:avl-1-4-2 ,avl-1-4-3)
2,插入到右子樹right、平衡因子爲-2,完全對稱
平衡因子是2的情況示圖如下
情況1示圖

avl-1-4-1

情況2示圖(1)

avl-1-4-2

情況2示圖(2)

avl-1-4-3

平衡因子是-2的情況和2的情況正好相反

插入

AVL的插入與BST完全相同,都是自頂向下的
檢測是否平衡並旋轉的調整過程
    1.AVL性質2決定了在檢測結點p是否平衡之前,必須先保證 左右子樹已經平衡
    2.子問題必須成立 推導出 總問題是否成立,則說明是自底向上(這個很重要,自底向上,在遞歸或者循環去實現左旋或者右旋都是自底向上的去計算高度(height),這樣計算高度纔不會出錯,所以在代碼中計算高度只右+1而沒有-1,先增的葉子節點的高度都是固定的1)
    3.有parent指針,直接向上回溯
    4.無parent指針,後續遍歷框架,遞歸
    5.無parent指針,棧實現非遞歸

實現 (以下AVL的代碼是基於BST代碼的,只是添加了使其平衡的代碼)
1.AVLEntry增加height屬性,表示樹的高度,平衡因子可以實時計算
2.單旋轉:右旋rotateRight、左旋rotateLeft
3.雙旋轉:先左後右firstLeftThenRight、先右後左firstRightThenLeft
4.實現非遞歸,需要輔助棧Stack,將插入時候所經過的路徑壓棧
5.插入調整函數fixAfterInsertion
6.輔助函數checkBalance,斷言AVL樹的平衡性,檢測算法的正確性 

AVLMap中添加height屬性,表示樹的高度,添加獲取節點高度的方法

/**
     * 返回一個結點的高度
     * @param p
     * @return
     */
    public int getHeight(AVLEntry<K,V> p){
        return p == null ? 0 : p.height;
    }

旋轉調整

單旋轉,右旋代碼實現

avl-1-2-1
/**
     * 右旋(單旋轉)
     * 該方法需要右返回值,因爲AVLEntry中沒有parent指針(JDK中是有parent指針的,所以不需要有返回值),旋轉之後它的新的根節點需要返回
     * @param p
     * @return
     */
    private AVLEntry<K,V> rotateRight(AVLEntry<K,V> p) {
        AVLEntry<K,V> left = p.left;
        p.left = left.right;
        left.right = p;
        p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
        left.height = Math.max(getHeight(left.left),p.height) + 1;
        return left;//新的根節點
    }

單旋轉,左旋代碼實現(和右旋完全對稱)
和圖:avl-1-2-1完全對稱

/**
     * 左旋(單旋轉)
     * @param p
     * @return
     */
    private AVLEntry<K, V> rotateLeft(AVLEntry<K, V> p) {
        AVLEntry<K,V> right = p.right;
        p.right = right.left;
        right.left = p;
        p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
        right.height = Math.max(p.height,getHeight(right.right)) + 1;
        return right;//新的根節點
    }

雙旋轉,先左旋再右旋代碼實現

avl-1-2-2
 /**
     * 先左旋再右旋
     * 先將p.left進行左旋,再將p進行右旋
     * @param p
     * @return
     */
    private AVLEntry<K,V> firstLeftThenRight(AVLEntry<K,V> p) {
        p.left = rotateLeft(p.left);
        p = rotateRight(p);
        return p;
    }

雙旋轉,先右旋再左旋代碼實現
和圖:avl-1-2-2完全對稱

/**
     * 先右旋再左旋
     * 先將p.right進行右旋,再將p進行左旋
     * @param p
     * @return
     */
    private AVLEntry<K, V> firstRightThenLeft(AVLEntry<K, V> p) {
        p.right = rotateRight(p.right);
        p = rotateLeft(p);
        return p;
    }

旋轉代碼寫完,下面實現插入平衡的代碼,要實現插入調整樹平衡,需要引入棧Stack,使用棧可以實現插入調整的非遞歸算法
private LinkedList<AVLEntry<K,V>> stack = new LinkedList<>();//用於實現插入調整的非遞歸算法

插入調整函數實現
插入的時候需要將其走的所有路徑不斷壓棧

public V put(K key,V value) {
        if (root == null) {
            root = new AVLEntry<K,V>(key,value);
            stack.push(root);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
            size ++;
        }else{
            AVLEntry<K,V> p = root;
            while (p != null) {
                stack.push(p);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
                int cmp = compare(key,p.key);
                if (cmp < 0) {
                    if (p.left == null) {
                        p.left = new AVLEntry<K,V>(key,value);
                        stack.push(p.left);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
                        size ++;
                        break;
                    }else{
                        p = p.left;//再次循環比較
                    }
                } else if (cmp > 0) {
                    if (p.right == null) {
                        p.right = new AVLEntry<K,V>(key,value);
                        stack.push(p.right);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
                        size ++;
                        break;
                    }else{
                        p = p.right;
                    }
                }else{
                    p.setValue(value);//替換舊值
                    break;
                }
            }
        }
        fixAfterInsertion(key);
        //不管是插入的是新值還是重複值,都返回插入的值,這個和JDK TreeMap不一樣
        return value;
    }
/**
     * 插入調整,使其二叉搜索樹達到平衡
     * @param key
     */
    private void fixAfterInsertion(K key){
        AVLEntry<K,V> p = root;
        while (!stack.isEmpty()) {
            p = stack.pop();//插入所走的路徑不斷彈棧
            p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
            int d = getHeight(p.left) - getHeight(p.right);//計算平衡因子
            if (Math.abs(d) <= 1) { //改樹平衡無需調整(旋轉)
                continue;
            }else{
                if (d == 2) {
                    if (compare(key, p.left.key) < 0) { //插入到了左子樹的左子樹
                        p = rotateRight(p);//單旋轉:右旋rotateRight
                    }else{//插入到了左子樹的右子樹
                        p = firstLeftThenRight(p); //雙旋轉:先左後右firstLeftThenRight
                    }
                }else{ //d == -2
                    if (compare(key, p.right.key) > 0) { //插入到了右子樹的右子樹
                        p = rotateLeft(p);//單旋轉:左旋rotateLeft
                    }else{//插入到了右子樹的左子樹
                        p = firstRightThenLeft(p);//雙旋轉:先右後左firstRightThenLeft
                    }
                }
                //旋轉過後,需要判斷走的是左子樹還是右子樹,也就是檢測爺爺結點,也就是p.parent要設置左子樹還是右子樹
                if (!stack.isEmpty()) {
                    if (compare(key, stack.peek().key) < 0) { //表明插入到了左子樹
                        stack.peek().left = p;
                    }else{
                        stack.peek().right = p;
                    }
                }
            }
        }
        root = p;//重新設置根節點
     }

插入調整插入調整優化

/**
     * 插入調整,使其二叉搜索樹達到平衡
     * @param key
     */
    private void fixAfterInsertion(K key){
        AVLEntry<K,V> p = root;
        while (!stack.isEmpty()) {
            p = stack.pop();//插入所走的路徑不斷彈棧
            //優化
            //**************************************************************
            int newHeight = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
            if (p.height > 1 /*保證p不是葉子節點*/ && newHeight == p.height/*高度沒有改變*/) {
                stack.clear();
                return;
            }
            //**************************************************************
            p.height = newHeight;//Math.max(getHeight(p.left),getHeight(p.right)) + 1;
            int d = getHeight(p.left) - getHeight(p.right);//計算平衡因子
            if (Math.abs(d) <= 1) { //改樹平衡無需調整(旋轉)
                continue;
            }else{
                if (d == 2) {
                    if (compare(key, p.left.key) < 0) { //插入到了左子樹的左子樹
                        p = rotateRight(p);//單旋轉:右旋rotateRight
                    }else{//插入到了左子樹的右子樹
                        p = firstLeftThenRight(p); //雙旋轉:先左後右firstLeftThenRight
                    }
                }else{ //d == -2
                    if (compare(key, p.right.key) > 0) { //插入到了右子樹的右子樹
                        p = rotateLeft(p);//單旋轉:左旋rotateLeft
                    }else{//插入到了右子樹的左子樹
                        p = firstRightThenLeft(p);//雙旋轉:先右後左firstRightThenLeft
                    }
                }
                //旋轉過後,需要判斷走的是左子樹還是右子樹,也就是檢測爺爺結點,也就是p.parent要設置左子樹還是右子樹
                if (!stack.isEmpty()) {
                    if (compare(key, stack.peek().key) < 0) { //表明插入到了左子樹
                        stack.peek().left = p;
                    }else{
                        stack.peek().right = p;
                    }
                }
            }
        }
        root = p;//重新設置根節點
     }

AVL插入平衡算法改進與時間複雜度分析
1,彈棧的時候,一旦發現某個節點的高度未發生改變,則立即停止回溯
2,指針回溯次數,最壞情況O(logN),最好情況O(1),平均任然是O(logN)
3,旋轉次數,無旋轉O(0),單旋轉O(1),雙旋轉O(2),不會超過兩次,平均O(1) (AVL樹插入旋轉不會超過兩次)
4,時間複雜度:BST的插入O(logN) + 指針回溯O(logN) + 旋轉O(1) = O(logN)
5,空間複雜度:有parent爲O(1),無parent爲O(logN)

插入平衡練習
將給定的排序數組轉化爲平衡二叉樹,左右子樹高度差的絕對值不超過1
實現方式1:使用AVLMap中的put實現方式
時間複雜度O(NlogN),空間複雜度O(N)

/**
 * 108(https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/)
 * 給定排序數組,將它轉化爲平衡二叉樹
 * 要求左右子樹高度差的絕對值不超過1(性質2)
 *
 * 實現方式1
 * AVLMap的put實現方式
 *
 */
public class ConvertSortedArrayToBinarySearchTree {

    class LeetCodeAVL{
        private int size;
        private TreeNode root;
        private LinkedList<TreeNode> stack = new LinkedList<>();

        public LeetCodeAVL() {
        }

        public int size() {
            return this.size;
        }

        public boolean isEmpty() {
            return this.size == 0;
        }

        public void put(int key) {
            if (root == null) {
                root = new TreeNode(key);
                stack.push(root);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
                size ++;
            }else{
                TreeNode p = root;
                while (p != null) {
                    stack.push(p);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
                    int cmp = key - p.val;
                    if (cmp < 0) {
                        if (p.left == null) {
                            p.left = new TreeNode(key);
                            size ++;
                            stack.push(p.left);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
                            break;
                        }else{
                            p = p.left;//再次循環比較
                        }
                    } else if (cmp > 0) {
                        if (p.right == null) {
                            p.right = new TreeNode(key);
                            size ++;
                            stack.push(p.right);//需要將put走的路徑全部壓棧,爲了fixAfterInsertion實現平衡
                            break;
                        }else{
                            p = p.right;
                        }
                    }else{
                        break;
                    }
                }
            }
            fixAfterInsertion(key);
        }

        private HashMap<TreeNode,Integer> heightMap = new HashMap<>();

        /**
         * 返回一個結點的高度
         */
        public int getHeight(TreeNode p) {
            return heightMap.containsKey(p) ? heightMap.get(p):0;
        }


        /**
         * 右旋(單旋轉)
         * 該方法需要右返回值,因爲AVLEntry中沒有parent指針(JDK中是有parent指針的,所以不需要有返回值),旋轉之後它的新的根節點需要返回
         * @param p
         * @return
         */
        private TreeNode rotateRight(TreeNode p) {
            TreeNode left = p.left;
            p.left = left.right;
            left.right = p;
            heightMap.put(p,Math.max(getHeight(p.left),getHeight(p.right)) + 1);
            heightMap.put(left,Math.max(getHeight(left.left),getHeight(p)) + 1);
            return left;//新的根節點
        }

        /**
         * 左旋(單旋轉)
         * @param p
         * @return
         */
        private TreeNode rotateLeft(TreeNode p) {
            TreeNode right = p.right;
            p.right = right.left;
            right.left = p;
            heightMap.put(p,Math.max(getHeight(p.left),getHeight(p.right)) + 1);
            heightMap.put(right,Math.max(getHeight(p),getHeight(right.right)) + 1);
            return right;//新的根節點
        }

        /**
         * 先左旋再右旋
         * 先將p.left進行左旋,再將p進行右旋
         * @param p
         * @return
         */
        private TreeNode firstLeftThenRight(TreeNode p) {
            p.left = rotateLeft(p.left);
            p = rotateRight(p);
            return p;
        }

        /**
         * 先右旋再左旋
         * 先將p.right進行右旋,再將p進行左旋
         * @param p
         * @return
         */
        private TreeNode firstRightThenLeft(TreeNode p) {
            p.right = rotateRight(p.right);
            p = rotateLeft(p);
            return p;
        }

        /**
         * 插入調整,使其二叉搜索樹達到平衡
         * @param key
         */
        private void fixAfterInsertion(int key){
            TreeNode p = root;
            while (!stack.isEmpty()) {
                p = stack.pop();//插入所走的路徑不斷彈棧
                int newHeight = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
                if (heightMap.containsKey(p) && getHeight(p) > 1 /*保證p不是葉子節點*/ && newHeight == getHeight(p)/*高度沒有改變*/) {
                    stack.clear();
                    return;
                }
                heightMap.put(p,newHeight);//Math.max(getHeight(p.left),getHeight(p.right)) + 1;
                int d = getHeight(p.left) - getHeight(p.right);//計算平衡因子
                if (Math.abs(d) <= 1) { //改樹平衡無需調整(旋轉)
                    continue;
                }else{
                    if (d == 2) {
                        if (key - p.left.val < 0) { //插入到了左子樹的左子樹
                            p = rotateRight(p);//單旋轉:右旋rotateRight
                        }else{//插入到了左子樹的右子樹
                            p = firstLeftThenRight(p); //雙旋轉:先左後右firstLeftThenRight
                        }
                    }else{ //d == -2
                        if (key - p.right.val > 0) { //插入到了右子樹的右子樹
                            p = rotateLeft(p);//單旋轉:左旋rotateLeft
                        }else{//插入到了右子樹的左子樹
                            p = firstRightThenLeft(p);//雙旋轉:先右後左firstRightThenLeft
                        }
                    }
                    //旋轉過後,需要判斷走的是左子樹還是右子樹,也就是檢測爺爺結點,也就是p.parent要設置左子樹還是右子樹
                    if (!stack.isEmpty()) {
                        if (key - stack.peek().val < 0) { //表明插入到了左子樹
                            stack.peek().left = p;
                        }else{
                            stack.peek().right = p;
                        }
                    }
                }
            }
            root = p;//重新設置根節點
        }
    }

    public TreeNode sortedArrayToBST(int[] nums){
        if (nums == null || nums.length == 0) { //邊界檢測
            return null;
        }
        LeetCodeAVL avl = new LeetCodeAVL();
        for (int num : nums) {
            avl.put(num);
        }
        return avl.root;
    }
}

實現方式2:遞歸構建AVL + BST
參考TreeMap中的buildFromSorted
時間複雜度O(N),空間複雜度O(logN)
二分快排歸併的遞歸算法實現方式二

avl-1-5-1
public class ConvertSortedArrayToBinarySearchTree {
   /**
     * 模仿TreeMap中的buildFromSorted
     * 時間複雜度O(N)
     * 空間複雜度O(logN)
     * @param nums
     * @return
     */
    public TreeNode sortedArrayToBST(int[] nums){
        if (nums == null || nums.length == 0) {
            return null;
        }
        return buildFromSorted(0,nums.length - 1,nums);
    }

    private TreeNode buildFromSorted(int lo, int hi, int[] nums) {
        if (hi < lo) {
            return null;
        }
        int mid = (lo + hi) / 2;
        TreeNode left = null;
        if (lo < mid) {
            left = buildFromSorted(lo, mid - 1, nums);
        }
        TreeNode middle = new TreeNode(nums[mid]);
        if (left != null) {
            middle.left = left;
        }
        if (mid < hi) {
            TreeNode right = buildFromSorted(mid + 1, hi, nums);
            middle.right = right;
        }
        return middle;
    }
}

計算完整二叉樹的高度
JDK TreeMap源碼中的通過節點個數計算樹的層數,實現原理使用的是二分法
時間複雜度O(logN)

private static int computeRedLevel(int sz) {
        int level = 0;
        for (int m = sz - 1; m >= 0; m = m / 2 - 1)
            level++;
        return level;
    }

刪除

AVL的刪除
AVL的刪除只需在BST的刪除基礎上加上刪除平衡即可
1,類似插入,假設刪除了p右子樹的某個結點,引起了p的平衡因子d[p]=2,分析p的左子樹left,三種情況如下:
    情況1:left的平衡因子d[left]=1,將p右旋  (圖:avl-1-6-1)

avl-1-6-1


    情況2:left的平衡因子d[left]=0,將p右旋  (圖:avl-1-6-2)

avl-1-6-2


    情況3:left的平衡因子d[left]=-1,先左旋left,再右旋p  (圖:avl-1-6-3)

avl-1-6-3


2,刪除左子樹,即d[p]=-2的情況,與d[p]=2對稱 

代碼實現
刪除節點後,調整該節點,使其整棵樹保持平衡

/**
     * 刪除調整
     * 1,類似插入,假設刪除了p右子樹的某個結點,引起了p的平衡因子d[p]=2,分析p的左子樹left,三種情況如下:
     *     情況1:left的平衡因子d[left]=1,將p右旋
     *     情況2:left的平衡因子d[left]=0,將p右旋
     *     情況3:left的平衡因子d[left]=-1,先左旋left,再右旋p
     * 2,刪除左子樹,即d[p]=-2的情況,與d[p]=2對稱
     *
     * 刪除算法是遞歸的,所以該方法是在遞歸中調用的
     * @param p
     * @return
     */
    private AVLEntry<K, V> fixAfterDeletion(AVLEntry<K, V> p) {
        if (p == null) return null;
        else{
            p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
            int d = getHeight(p.left) - getHeight(p.right);
            if (d == 2) { //說明p.left一定不爲null
                if (getHeight(p.left.left) - getHeight(p.left.right) >= 0) {
                    p = rotateRight(p);
                }else{
                    p = firstLeftThenRight(p);
                }
            } else if (d == -2) {//說明p.right一定不爲null
                if (getHeight(p.right.right) - getHeight(p.right.left) >= 0) {
                    p = rotateLeft(p);
                }else{
                    p = firstRightThenLeft(p);
                }
            }
            return p;
        }
    }

源碼:
https://github.com/xiaojinwei/java-learning/blob/master/src/com/cj/learn/tree/avl/AVLMap.java

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