红黑树及其相关操作(一遍包会)

红黑树

通过上一问我们已经清楚的了解了AVL树及其相关操作,并对这些特殊的BST有了一些认识,接下来我们将见识这个世界的神奇——红黑树

AVL树及其相关操作地址
AVL树

还记得下面这颗我们几乎做不到的完美平衡树吧:

完美BST
这样的二叉树我们都知道是很难也没必要的,所以人类的执着让我们发现AVL这种自平衡的二叉树,随着人类的进化,人们发现AVL树虽然是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效率的时间复杂度,即Olog(N);但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。所以他很难胜任修改操作多的情况,于是乎第二种自平衡二叉树(红黑树)。。。。嗯,客套话就不乱吹来直接来认识一下吧:

红黑树性质(当然他满足BST的性质,因为本质是一颗特殊的BST)

  1. 每个节点都有颜色、红色或黑色
  2. 根节点是黑色
  3. 每个叶子节点(NIL)都是黑色
    nil 节点就是空节点,在红黑树的实现中,nil 节点代替二叉树中的 NULL:叶子节点的左节点和右节点指针都指向 nil 节点;只一个子树的节点,其另外一个子节点指针也指向 nil 节点;根节点的父节点也指向 nil 节点。nil节点的父节点和右节点都是自己,左节点为红黑树的根节点。如果红黑树为空(没有根节点),那么nil节点的左节点就也是自己。nil节点的存在大大方便了很多操作。
    在这里插入图片描述
  4. 如果一个节点是红的、那么他的子节点必须是黑的
  5. 任意一个节点到该节点每个叶子节点的所有路径上包含相同的黑节点个树(所以他保证最长的路径最多的最短路径的两倍)

红黑树1
红黑树2

到这里应该已经认识红黑树了,那么我们来看看他具体是如何操作的

首先认识两个旋转操作,与AVL树基本一致可以有兴趣可以看看AVL树

左旋操作

private void turnLeft(RBNode avl) {
        RBNode cur = avl.right;
        avl.right = cur.left;
        if (cur.left != null){
            cur.left.parent = avl;
        }
        cur.parent = avl.parent;
        //当然有可能原本的父节点是根没有爷爷节点,所以交换后,此时的节点即(avl.right)变成头节点
        if (avl.parent == null){
            root = cur;
        }
        if (avl.parent.left == avl){
            avl.parent.left = cur;
        }else {
            avl.parent.right = cur;
        }
        cur.left = avl;
        avl.parent = cur;
    }

左旋示意图

右旋操作

private void turnRight(RBNode avl) {
        RBNode cur = avl.left;
        avl.left = cur.right;
        if (cur.right != null){
            cur.right.parent = avl;
        }
        cur.parent = avl.parent;
        if (avl.parent == null){
            root = cur;
        }
        if (avl.parent.left == avl){
            avl.parent.left = cur;
        }else {
            avl.parent.right = cur;
        }
        cur.right = avl;
        avl.parent = cur;
    }

右旋示意图
发现了吧和AVL树的旋转操作不说略有所同只说一模一样,到现在也能理解为什么AVL树中LL状态操作要叫右单旋了吧,他是基于参考节点root的位置变化
这个不应该有问题哦是AVL树的操作,不明白的话可以反回去看一下AVL树


接下来我们真是介绍红黑树的插入操作

首先我们要理解红黑树与AVL树一样都是特殊的BST所以他的大概操作基本是与BST一致的只是多了一些维持操作

  1. 插入节点(与BST一致)并加入颜色让他为红,这样可以避免黑色数量不同方便操作
    2.进行红黑树维持insertFixUp()操作

而我们的重点就是维持这棵树时的歌从情况对应的操作

  1. 插入时树是空的,那我也直接插入,把根放成他就好了
  2. 插入节点的父亲是黑的(特殊说明:我没有种族歧视哟,不要告我),啥也不用做,因为插入的是红的;
    插入节点是黑
  3. 插入时父亲节点是红的,这就相对比较麻烦了,我们又要分情况
    首先这种情况要分为两个对称操作的部分,即父节点是爷爷节点的左孩子或者右孩子;如下图例我们以左孩子为例操作,后文将附全部代码
    1.插入节点父节点的兄弟节点也就是插入节点的叔叔节点也是红的
    我们让他的父节点与叔叔节点变黑、爷爷节点变红,并且把爷爷节点作为新的当前节点继续操作,因为如图,爷爷节点违反规则了(这波叫孙债爷偿)
    父红、叔红
    2. 插入节点叔叔节点是黑的,并且当前节点是父节点右孩子
    我们以父节点为支点左旋,如下图
    左旋示意图
    此时我们可以看到旋转后仍未返规则,不过此时的状态也就是我们要介绍的第三种情况
    3. 叔叔节点是黑,并且当前节点是父节点左子树
    我们
    1、先将父节点涂黑;
    2、再将爷爷节点涂红
    3、最后以当前节点爷爷节点为支点右旋
    右旋示意图
  4. 所有操作完成后根节点涂黑

这是维持操作代码可以稍作理解后续将附完整代码

private void insertFixUp(RBNode avl) {
        //千万不要这么写哦,如果吧父亲和爷爷节点定义在前面,那怎么循环呢?
        // RBNode parent  = avl.parent, grandParent = parent.parent;
        RBNode parent , grandParent;
        while ((parent = avl.parent) != null && parent.color == red){
            grandParent = parent.parent;
            if (parent == grandParent.left){
                RBNode uncle = grandParent.right;
                if (uncle != null && uncle.color == red){
                    parent.color = black;
                    uncle.color = black;
                    grandParent.color = red;
                    avl = grandParent;
                    continue;//记得要冲这里跳出到下一个执行哦
                }
                if (avl == parent.right){
                    //跟AVL左旋操作如出一辙
                    turnLeft(parent);
                    RBNode cur = parent;
                    parent = avl;
                    avl = cur;
                }
                parent.color = black;
                grandParent.color = red;
                turnRight(grandParent);
            }else {
                //父节点是当前爷爷节点的右子树
                RBNode uncle = grandParent.left;
                if (uncle != null && uncle.color == red){
                    parent.color = black;
                    uncle.color = black;
                    grandParent.color = red;
                    avl = grandParent;
                    continue;
                }
                if (avl == parent.left){
                    turnRight(parent);
                    RBNode cur = parent;
                    parent = avl;
                    avl = cur;
                }
                parent.color = black;
                grandParent.color = red;
                turnLeft(grandParent);
            }
        }
        //不管怎么样,操作完之后跟节点有可能不黑,我们给他涂黑就好了
        root.color = black;
    }

想必此时我们已经基本理解了红黑树的插入操作,那么接下来我们看看他完整的样子:

注:此代码我还没有测试,不过我感觉有bug,因为节点有可能是空,但是我的所有颜色及父节点操作都是直接修改可能会异常,并且很多地方没考虑引用问题,所以此代码仅供学习参考,后续找到工作,闲了将进行测试并附上完整代码


    public void insert(int val){
        RBNode rbVal = new RBNode(val , black);
        root = insert(root , rbVal);
    }

    private RBNode insert(RBNode root, RBNode avl) {
        if (root == null){
            root = avl;
        }else {
            if (root.val > avl.val){
                root.left = insert(root.left , avl);
                avl.color = red;
                avl.parent = root;
                insertFixUp(avl);
            }else if (root.val < avl.val){
                root.right = insert(root.right , avl);
                avl.color = red;
                avl.parent = root;
                insertFixUp(avl);
            }else {
                System.out.println("已经有这个值了不要重复添加");
            }
        }
        return root;
    }

    private void insertFixUp(RBNode avl) {
        //千万不要这么写哦,如果吧父亲和爷爷节点定义在前面,那怎么循环呢?
        // RBNode parent  = avl.parent, grandParent = parent.parent;
        RBNode parent , grandParent;
        while ((parent = avl.parent) != null && parent.color == red){
            grandParent = parent.parent;
            if (parent == grandParent.left){
                RBNode uncle = grandParent.right;
                if (uncle != null && uncle.color == red){
                    parent.color = black;
                    uncle.color = black;
                    grandParent.color = red;
                    avl = grandParent;
                    continue;//记得要冲这里跳出到下一个执行哦
                }
                if (avl == parent.right){
                    //跟AVL左旋操作如出一辙
                    turnLeft(parent);
                    RBNode cur = parent;
                    parent = avl;
                    avl = cur;
                }
                parent.color = black;
                grandParent.color = red;
                turnRight(grandParent);
            }else {
                //父节点是当前爷爷节点的右子树
                RBNode uncle = grandParent.left;
                if (uncle != null && uncle.color == red){
                    parent.color = black;
                    uncle.color = black;
                    grandParent.color = red;
                    avl = grandParent;
                    continue;
                }
                if (avl == parent.left){
                    turnRight(parent);
                    RBNode cur = parent;
                    parent = avl;
                    avl = cur;
                }
                parent.color = black;
                grandParent.color = red;
                turnLeft(grandParent);
            }
        }
        //不管怎么样,操作完之后跟节点有可能不黑,我们给他涂黑就好了
        root.color = black;
    }

    private void turnRight(RBNode avl) {
        RBNode cur = avl.left;
        avl.left = cur.right;
        if (cur.right != null){
            cur.right.parent = avl;
        }
        cur.parent = avl.parent;
        if (avl.parent == null){
            root = cur;
        }
        if (avl.parent.left == avl){
            avl.parent.left = cur;
        }else {
            avl.parent.right = cur;
        }
        cur.right = avl;
        avl.parent = cur;
    }

    private void turnLeft(RBNode avl) {
        RBNode cur = avl.right;
        avl.right = cur.left;
        if (cur.left != null){
            cur.left.parent = avl;
        }
        cur.parent = avl.parent;
        //当然有可能原本的父节点是根没有爷爷节点,所以交换后,此时的节点即(avl.right)变成头节点
        if (avl.parent == null){
            root = cur;
        }
        if (avl.parent.left == avl){
            avl.parent.left = cur;
        }else {
            avl.parent.right = cur;
        }
        cur.left = avl;
        avl.parent = cur;
    }

到这里完整的插入我们已经理解了我想你应该有如下几个问题(没有的话好兄弟,给你一张反思券,好好反思一下为啥没发现,文章最后有具体操作的完整代码可供参考)

  1. 为啥插入节点要变成红的
    解:红黑树性质第五条:任意一个节点到该节点每个叶子节点的所有路径上包含相同的黑节点个树,如果插的是黑的。。。。。
  2. 为啥不直接插入红,而是插入后置为红
    解:主要是我们写的方法,如果树一开始是空我们插一个节点当树根的时候方便。
    3为啥两个父节点和叔叔节点都是红的话要先把他两变黑,爷爷变红再进行旋转
    解:如果父节点是红的话我们插入一个红的嘎违背了规则4.“ 如果一个节点是红的、那么他的子节点必须是黑的”所以我们把父亲涂黑,但是涂黑后我们又违背规则五,因为当前路径多了一个黑的,那么他爷爷到他爸爸这条路径就多了一个黑,所以我们把父亲再把父亲变红,然而做完这部我们会发现叔叔节点又少了一个黑,所以我们把叔叔变黑。
  3. 为啥上一步的爷爷节点要变红,咱们知道爷爷是黑的
    解:这里没有基因学说,是因为我们的操作是基于父节点是红树操作,父亲是红爷爷只能是黑因为红节点子节点只能是黑,如果爷爷红了那么爹肯定黑了(反正就是你俩不是一个颜色,才能证明是亲生的,啊,真贱啊这棵树)
    到这里你也应该能看出来为啥要以爷爷节点继续操作了,因为我们的步骤保障了,儿子,爸爸,叔叔节点不违反规则,但是爷爷节点就不一定了所以继续以爷爷为新节点继续判断
    4.爹是红的,叔叔是黑的,为啥要旋转
    解:这里我们是分情况了子树是父亲右和左,而在右的左旋操作其实就是为了达到第三种状态,所以旋转这俩步其实可以算是一个步骤,而达到状态三也就是,子树是父的左树,此时与状态二一样,我们仍然再违背规则4,完了规则4是啥了?(如果一个节点是红的、那么他的子节点必须是黑的)所以我们再把爸爸涂黑爷爷涂红,是为了完成右旋后满足规则5,然后进行右旋

这里是不是放个图好一点,哈哈哈其实如果理清的话这个图你身边的纸上应该已经画出来了,所以。。。。
嗯。。。图如下可供参考
旋转
右旋图:F涂黑此时多一个黑,再把G涂红,此时G->U这条路少一个黑,所以右旋让左边多出来的黑当左右的公共黑爸爸即把他置为根

好了到这里红黑树的插入我们就告一段了,解下了,再放一个大招,红黑树的删除哈哈哈哈哈哈哈哈!后悔学计算机吧,文章写到这里我掉毛已经比我家猫严重了。


红黑树删除:
首先来讲一段废话(理论知识及删除操作的大概思想、如果有基础的话也看看吧,不然。。。。)

  1. 永远记住RBTree与ABLTree都是特殊的BST,他仍然满足BST特性删除操作也基于BST只是在删除后加入修正操作来维持RBTree(不是修正药业哟~)
  2. 为了方便我们把删除分为四种情况
    1. 左右节点都是空
    如果此时这个节点没爹,又因为没儿子所以,只有这棵树只有这一个节点,我们置空,如果有爹,则判断当前节点是父节点左还是右孩子然后置空,最后执行修正维持操作
    2.只有左孩子
    如果当前节点没爹,就是树根,让儿子当树根就好,如果有爹,把左孩子与爹连起来;最后执行修正维持操作
    3. 只有右孩子
    同自由左孩子类似
    4. 左右孩子都不是空
    找到前驱或者后继节点进行替换,然后在相应的树中删除被替换的节点(这样就会把复杂的删除两个节点的过程变成删除前驱/后继节点因为前驱或后继最多只有一个孩子哟~)这个操作与AVL树一样可以看一下AVL树
  3. 想必家人们已经发现了,每一种情况完成后都要执行修正操作来维持RBtree这也是最难的部分,我们后续将介绍
//这是删除框架、后续会有完整代码
private void delete(RBNode avl){
        if (avl.left != null && avl.right != null){
            deleteDoubleChild(avl);
        }
        if (avl.left == null && avl.right == null){
            deleteNoChild(avl);
        }
        if (avl.left != null){
            deleteLeftChild(avl);
        }
        if (avl.right != null){
            deleteRightChild(avl);
        }
    }

上面那个是操作框架后文会附完整代码,我们接下来看看删除的修正操作,当然再了解这个之前,我们再来回顾一下RBT的五条性质规则:

  1. 每个节点都有颜色、红色或黑色
  2. 根节点是黑色
  3. 每个叶子节点(NIL)都是黑色
  4. 如果一个节点是红的、那么他的子节点必须是黑的
  5. 任意一个节点到该节点每个叶子节点的所有路径上包含相同的黑节点个树

很明显了吧我们几乎删除节点有可能使2、4、5规则不成立所以。。。
接下来请看操作:
如果对红黑树有过了解我们可能听说过额外黑这种说法,其实就是已经删掉的那个节点我们默认是黑,如果现在的节点是红就是“红+黑”,如果现在的是黑就是“黑+黑”,后面会解释问什么这样,(避免规则2)我们后面会总结这个东西,现在先带大家认识一下各种情况,我们目前记住删掉的节点我们默认是黑,新加的节点我们把这种黑给他保留在新加节点中(当然他是不存在的只是一种想象)
这时大概会有两大种情况,即当前节点也就是删掉的节点是原树的左或右孩子,因为两种情况与插入时一样是对称操作,所以我们以左孩子为例又可以分别三种情况:

情况1:删除节点的继承节点是红,那么我们记他为“红+黑”
在这里插入图片描述

情况2:这个新的节点,也就是删掉节点的子节点是黑的并且这个子节点当了树根(删掉的是根吗?思考一下)在这里插入图片描述
情况3:这个新节点是黑的,即“黑+黑,且没当树根”,这个我们又要将其分为四种情况

  • 当前节点是“B+B”,且兄弟的节点是红的
    在这里插入图片描述

  • 当前节点是“B+B”,且兄弟节点是黑的,且兄弟节点的两个子节点也是黑的
    在这里插入图片描述

  • 当前节点“B+B”,且兄弟是黑的,且兄弟右树是黑的,左树是红的
    (什么问我左树是黑怎么办?怎么办?这话问的出来,那不就是左右都黑吗?)
    在这里插入图片描述

  • 当前节点“B+B”,且兄弟是黑的,且右子树是红的(不考虑左子树)
    在这里插入图片描述
    现在我们基本了解了删除操作,来上手体验一下代码吧:

public void delete(int val){
        RBNode avl = find(root , val);
        if (avl != null){
            delete(avl);
        }else {
            System.out.println("节点不存在,删个毛线");
        }
    }

    private void delete(RBNode avl){
        if (avl.left != null && avl.right != null){
            deleteDoubleChild(avl);
        }
        if (avl.left == null && avl.right == null){
            deleteNoChild(avl);
        }
        if (avl.left != null){
            deleteLeftChild(avl);
        }
        if (avl.right != null){
            deleteRightChild(avl);
        }
    }

    private void deleteRightChild(RBNode avl) {
        RBNode child = avl.right;
        RBNode parent = avl.parent;
        boolean color = avl.color;
        if (avl.parent != null){
            if (parent.left == avl){
                parent.left = child;
                child.parent = parent;
            }else {
                parent.right = child;
                child.parent = parent;
            }
        }else {
            root = avl.right;
        }
        if (color == black){
            deleteFixUp(child , parent);
        }
        return;
    }

    private void deleteLeftChild(RBNode avl) {
        RBNode child = avl.left;
        RBNode parent = avl.parent;
        boolean color = avl.color;
        if (avl.parent != null){
            if (parent.left == avl){
                parent.left = child;
                child.parent = parent;
            }else {
                parent.right = child;
                child.parent = parent;
            }
        }else {
            root = avl.left;
        }
        if (color == black){
            deleteFixUp(child , parent);
        }
        return;

    }

    private void deleteNoChild(RBNode avl) {
        RBNode child = null;
        RBNode parent = avl.parent;
        boolean color = avl.color;
        if (avl.parent != null){
            if (avl.parent.left == avl){
                avl.parent.left = null;
            }else {
                avl.parent.right = null;
            }
        }else {
            root = null;
        }
        if (avl.color == black){
            //红节点不用管,删了不影响RBTree只有删黑点的时候要修正
            deleteFixUp(child , parent);
        }
        return;
    }

    private void deleteDoubleChild(RBNode avl) {
        //找到右子树的最大即后继节点
        RBNode rep = min(avl.right);
        avl.val = rep.val;
        delete(rep);
        return;
    }
    private void deleteFixUp(RBNode avl , RBNode parent) {
        RBNode brother;
        while ((avl == null || avl.color == black) && avl != root){
            if (avl == parent.left){
                brother = parent.right;
                if (brother.color == red){
                    brother.color = black;
                    parent.color = red;
                    turnLeft(parent);
                    brother = parent.right;
                }
                //这步走完兄弟肯定黑
                if ((brother.left == null || brother.left.color == black) && (brother.right == null || brother.right.color == black)){
                    brother.color = red;
                    avl = parent;
                    parent = parent.parent;
                }else {
                    if (brother.right.color == black){
                        //说明左树是红的
                        brother.left.color = black;
                        brother.color = red;
                        turnRight(brother);
                        brother = parent.right;
                    }
                    //截止这里他兄弟右树肯定红了,我怎么知道?你猜上一步旋转是为了什么
                    brother.color = parent.color;
                    parent.color = black;
                    brother.right.color = black;
                    turnLeft(parent);
                    avl = root;//其实下一圈循环也就跳出了
                    break;
                }
            }else {
                brother = parent.left;
                if (brother.color == red){
                    brother.color = black;
                    parent.color = red;
                    turnRight(parent);
                    brother = parent.left;
                }
                if ((brother.left == null || brother.left.color == black) && (brother.right == null || brother.right.color == black)){
                    brother.color = red;
                    avl = parent;
                    parent = parent.parent;
                }else {
                    if (brother.left.color == black){
                        brother.right.color = black;
                        brother.color = red;
                        turnLeft(brother);
                        brother = parent.left;
                    }
                    brother.color = parent.color;
                    parent.color = black;
                    brother.left.color = black;
                    turnRight(parent);
                    avl = root;
                    break;
                }
            }
            //到现在avl要么是树根,要么一开始就是红的我们把他不管怎样涂黑
            if (avl != null){
                avl.color = black;
            }
        }
    }
    private RBNode min(RBNode root){
        if (root.left == null){
            return root;
        }
        return min(root.left);
    }
    private RBNode max(RBNode root){
        if (root.right == null){
            return root;
        }
        return max(root.right);
    }

删除总结:其实这个东西就像转魔方,情况3的四种情况都是魔方中可能发生的路径,但是正确的解也就是唯一出口只有第四种,所以我们就不断的迭代进行旋转变化,直到兄弟是黑,且远离当前节点的兄弟子树是红时,我们执行秘籍(魔方也是第三层才有秘籍哟)【当然有些外挂玩家不要秘籍一个脑子想21个块比如情况2在满足情况时也会停止迭代】

到这里完整的删除我们已经理解了,我想你应该依然是有如下几个问题(没有的话好兄弟,我再给你一张反思券,好好反思一下为啥没发现,嗯?为什么还会发现不了问题?文章最后有具体操作的完整代码可供参考)

  1. 为什么修正操作孩子节点有一个,我们怎么选择
    解:其实仔细看不难发现,如果有两个孩子我们通过替换操作,最后会变成删他的前驱或后继节点、而前驱或后继节点最多只能有一个孩子。所以这也是为什么两个孩子的删除我们没有修正操作。
  2. 为什么要加入默认黑这个思想(这个东西想不起来提问就有问题了)
    解:这样可以避免违反规则二:根节点是黑节点但是思想上我们违背了规则一,因为我们加入了一个特殊黑色,当然这是在想象上,所以。。。
  3. 有没有发现为什么B+B且兄弟是红的时候最后要让叔叔节点变成父节点右边
    解:这个问题画图啊宝儿,不画图当然看不懂,其实叔叔一开始是父节点的右子树但是左旋后他已经变成了爷爷了,所以重置一下兄弟节点,以便后续操作
  4. 为什么处理完兄弟是红后,兄弟节点一定黑
    解:看到代码不是else了吧,那为啥不画个图呢,因为旋转后兄弟的左子树成了当前节点的兄弟,但是兄弟以前是红的,规则4:红节点子树一定黑
  5. 唯一出口只有情况4吗?
    解:我们要理解本质,当满足红黑树规则时我们就不进行迭代了,而之所以迭代是因为当前的一些操作影响到了红黑树的性质,所以要对影响到的位置一步一步迭代排查。其实第二种可能在满足条件时也会停止而情况三和时其实可以算在一起,因为情况3的解决一定会到情况4,情况三的操作也单纯时为了变换到情况4

最难理解的情况就是4,就像玩魔方,和找女朋友,不要局限于表面,试试换个思维,从C,E的黑节点个数入手,取推理,你会发现这棵树砸一眼看过去违反规则了,但是仔细推理你会发现每一个地方都出奇的合理,就像一些爱情故事,看上去疯狂男主又丑又老又矮又挫但是却赢走了你女神的芳心,而我们一直注意不到他可能是青蛙王子,家财万贯,温柔善良。不要局限于眼前,动手推理一下
在这里插入图片描述

截止目前,红黑树我们告一段落,本文完整代码将至于Gitee仅供学习参考切勿搬运,后续将更新测试,并且更新网络、线程及spring的内容,敬请期待

  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一哑7

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值