LeetCode部分习题解答记录-栈与队列篇

20.有效的括号

  • 方法1:首先,如果字符的长度为奇数,说明肯定不匹配;通过Map集合建立符号的匹配;便利字符串,将左括号压入,右括号进行判断
class Solution {
    public boolean isValid(String s) {
        int n = s.length();
        if(n % 2 == 1){
            return false;
        }
        Map<Character,Character> map = new HashMap<>();
        map.put(')','(');
        map.put(']','[');
        map.put('}','{');
        Deque<Character> stack = new LinkedList<>();
        for(int i = 0; i < n ;i++){
            char ch = s.charAt(i);
            if(map.containsKey(ch)){
                if(stack.isEmpty() || stack.peek() != map.get(ch))
                    return false;
                stack.pop();
            }else{
                stack.push(ch);
            }
        }
        return stack.isEmpty();
    }
}

150.逆波兰表达式求值

  • 方法1:将数字压入stack,碰到符号则取出两个数字,进行运算后压入stack。
class Solution {
    public int evalRPN(String[] tokens) {
        if(tokens.length == 1){
            return Integer.parseInt(tokens[0]);
        }
        int result = 0;
        Deque<Integer> stack = new LinkedList<>();
        for(int i = 0 ; i < tokens.length ; i++){
            String ch = tokens[i];
            if(isDigit(ch)){
                stack.push(Integer.parseInt(ch));
            }else{
                int c2 = stack.pop();
                int c1 = stack.pop();
                result = operation(c1,c2,ch);
                stack.push(result);
            }
        }
        return result;
    }

    public boolean isDigit(String s){
        if(s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")){
            return false;
        }
        return true;
    }

    public int operation(int num1,int num2,String op){
        if(op.equals("+")){
            return num1+num2;
        }
        if(op.equals("-")){
            return num1-num2;
        }
        if(op.equals("*")){
            return num1*num2;
        }
        if(op.equals("/")){
            return num1/num2;
        }
        return 0;
    }
}

71.简化路径

  • 方法1:首先将字符串按"/“分割,则分割数组指挥出现”.","…",或者空。 根据符号相应的定义进行压栈出栈。最后因为栈的先进后出特性,将出栈元素需要在最前面拼接字符串,因此使用insert。
class Solution {
    public String simplifyPath(String path) {
        String[] pathArray = path.split("/");
        Deque<String> stack = new LinkedList<>();
        for(int i = 0 ; i < pathArray.length ; i++){
            if(pathArray[i].equals("..")){
                if(!stack.isEmpty()){
                    stack.pop();
                }
            }else if(!pathArray[i].equals(".") && !pathArray[i].equals("")){
                stack.push(pathArray[i]);
            }
        }
        StringBuilder sb = new StringBuilder();
        if(stack.isEmpty()){
            return "/";
        }
        while(!stack.isEmpty()){
            sb.insert(0,stack.pop());
            sb.insert(0,"/");
        }
        return sb.toString();
    }
}

144.二叉树的前序遍历

  • 方法1:递归实现
class Solution {
    List<Integer> list = new ArrayList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        pre(root);
        return list;
    }
    public void pre(TreeNode root){
        if(root == null){
            return;
        }
        list.add(root.val);
        pre(root.left);
        pre(root.right);
    }
}
  • 方法2:非递归实现
class Solution {
 public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if(root == null){
            return list;
        }
        Deque<TreeNode> stack = new LinkedList<>();
        while(!stack.isEmpty() || root != null){
            while(root != null){
                list.add(root.val);
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            root = root.right;
        }
        return list;
    }

}

94.二叉树的中序遍历

  • 方法1:递归实现
class Solution {
  	public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        inorder(root,list);
        return list;
    }
    public void inorder(TreeNode root,List<Integer> list){
        if(root == null){
            return;
        }
        inorder(root.left,list);
        list.add(root.val);
        inorder(root.right,list);
    }
}
  • 方法2:非递归实现
public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if(root == null){
            return list;
        }
        Deque<TreeNode> stack = new LinkedList<>();
        while(!stack.isEmpty() || root != null){
            while(root != null){
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            list.add(root.val);
            root = root.right;
        }
        return list;
    }

145.二叉树的后序遍历

  • 方法1:递归实现
class Solution {
	public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        post(root,list);
        return list;
    }
    public void post(TreeNode root, List<Integer> list){
        if(root == null) return;
        post(root.left,list);
        post(root.right,list);
        list.add(root.val);
    }
}
  • 方法2:非递归实现。第三次经过根节点节点才会读取其数值。
class Solution {
 public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if(root == null){
            return list;
        }
        TreeNode pre = null;
        Deque<TreeNode> stack = new LinkedList<>();
        while(!stack.isEmpty() || root != null){
            while(root != null){
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            //root.right == pre,确保右分支处理完成后不会再进入,导致死循环
            if(root.right == null || root.right == pre){
                list.add(root.val);
                pre = root;
                root = null;
            }else{
                stack.push(root);
                root = root.right;
            }
        }
        return list;
    }
}

341.扁平化嵌套列表迭代器

  • 方法1:
public class NestedIterator implements Iterator<Integer> {
    LinkedList<NestedInteger> list;
    public NestedIterator(List<NestedInteger> nestedList) {
        list = new LinkedList<>(nestedList);
    }

    @Override
    public Integer next() {
        return list.remove(0).getInteger();
    }

    @Override
    public boolean hasNext() {
        //确定list首元素不是整数
        while(!list.isEmpty() && !list.get(0).isInteger()){
            List<NestedInteger> first = list.remove(0).getList();
            //将first从尾到头依次头插
            for(int i = first.size() - 1;i>=0 ;i--){
                list.addFirst(first.get(i));
            }
        }
        return !list.isEmpty();
    }
}

102.二叉树的层序遍历

  • 方法1:
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Deque<TreeNode> deque = new LinkedList<>();
        List<List<Integer>> list = new ArrayList<>();
        if(root == null){
            return list;
        }
        deque.offer(root);
        while(!deque.isEmpty()){
            List<Integer> curList = new ArrayList<>();
            //表示在当前层有几个数据,不能在循环体中使用,因为每一个p都会再判断压入子节点
            int curListSize = deque.size(); 
            for(int i = 0 ; i < curListSize; i++){
                TreeNode p = deque.poll();
                curList.add(p.val);
                if(p.left != null) deque.offer(p.left);
                if(p.right != null) deque.offer(p.right);
            }
            list.add(curList);
        }
        return list;
    }
}
  • 方法2:递归实现。
class Solution {
 public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> list = new ArrayList<>();
        dfs(root,1,list);
        return list;
    }
    public void dfs(TreeNode node,int depth,List<List<Integer>> list){
        if(node == null) return;
        //第一次遍历到当前层,给list添加新list
        if(list.size() < depth) list.add(new ArrayList<>());
        //通过取当前depth来进行对相应的list进行插入元素
        list.get(depth - 1).add(node.val);
        dfs(node.left,depth+1,list);
        dfs(node.right,depth+1,list);
    }
}

103.二叉树的锯齿形层次遍历

  • 方法1:非递归实现。在层次遍历的基础上加入了判断当前层数的奇偶性来进行头插或者尾插
class Solution {
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
       Deque<TreeNode> deque = new LinkedList<>();
        List<List<Integer>> list = new ArrayList<>();
        if(root == null){
            return list;
        }
        deque.offer(root);
        int index = 1;
        while(!deque.isEmpty()){
            int curSize = deque.size();
            List<Integer> curList = new ArrayList<>();
            for(int i = 0 ; i < curSize ; i++){
                TreeNode p = deque.poll();
                if(index % 2 == 0){
                    curList.add(0,p.val);
                }else{
                    curList.add(p.val);
                }
                if(p.left != null) deque.offer(p.left);
                if(p.right != null) deque.offer(p.right);          
            }
            index++;
            list.add(curList);
        }
        return list;
    }
}
  • 方法2:递归实现。与102题的递归实现类似,还是在其基础上判断当前层的奇偶性来进行相应操作。
//通过递归实现深度优先遍历完成
class Solution {
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
        List<List<Integer>> list = new ArrayList<>();
        dfs(root,1,list);
        return list;
    }
    public void dfs(TreeNode node,int depth,List<List<Integer>> list){
        if(node == null) return;
        //第一次遍历到当前层,给list添加新list
        if(list.size() < depth) list.add(new ArrayList<>());
        //通过取当前depth来进行对相应的list进行插入元素
        if(depth % 2 == 1){
            list.get(depth - 1).add(node.val);
        } else{
            list.get(depth - 1).add(0,node.val);
        }
        dfs(node.left,depth+1,list);
        dfs(node.right,depth+1,list);
    }
}

199.二叉树的右视图

  • 方法1:非递归实现。
class Solution {
    // BFS非递归实现
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        Deque<TreeNode> deque = new LinkedList<>();
        if(root == null){
            return list;
        }
        deque.offer(root);
        while(!deque.isEmpty()){
            int curSize = deque.size();
            for(int i = 0 ; i < curSize;i++){
                TreeNode p =  deque.poll();
                if(p.left != null) deque.offer(p.left);
                if(p.right != null) deque.offer(p.right);
                if(i == curSize -1){
                    list.add(p.val);
                }
            }
        }
        return list;
    }
}
  • 方法2:递归实现。先遍历右节点,再遍历左节点。因此,当list的大小等于深度时,说明遍历到当前层的最右侧节点,则压入list。
 //DFS递归实现
 class Solution {
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        dfs(root,0,list);
        return list;
    }
    public void dfs(TreeNode node, int depth,List<Integer> list){
        if(node == null) return;
        if(list.size() == depth) list.add(node.val);
        dfs(node.right,depth + 1,list);
        dfs(node.left,depth + 1,list);
    }

279.完全平方数

  • 方法1:最优解。基于图的解法。用step来记录从起始节点到当前节点对应的步数(即,经过了几个完全平方数)。用flag来记录当前节点是否为第一次访问,避免重复操作。
class Solution {
    public int numSquares(int n) {
        Deque<Integer> deque = new LinkedList<>();
        int[] step = new int[n+1];  //从起始值到达某值的步数
        //确保数值只有首次计算时推入队列,例如12,12-2*2=8,同时经过12-1*1-1*1-1*1-1*1=8,这两种情况,12-2*2肯定是为首选。
        boolean[] flag = new boolean[n+1];
        deque.offer(n);
        step[n] = 0;
        flag[n] = true;
        while(!deque.isEmpty()){
            int num = deque.poll();
            int s = step[num];
            if(num == 0){
                return step[num];
            }
            for(int i = 0 ; ;i++){
                int a = num - i * i;
                if(a < 0 ) break;
                if(a == 0) return step[num] + 1;
                if(!flag[a]){
                    deque.offer(a);
                    step[a] = s + 1;
                    flag[a] = true;
                }
            }
        }
        return -1;       
    }
}
  • 方法2:在方法1的基础上,更改一些代码。不用标记flag,而用一个集合来存储访问过的节点。该方法所用时间比方法1多,因为在判断当前节点是否遍历过时,方法1所用时间为O(1),而set中contains方法虽然还为O(1),但是所需时间会相对多一些。
class Solution {
	public int numSquares(int n) {
        Deque<Integer> deque = new LinkedList<>();
        deque.offer(n);
        
        //确保数值只有首次计算时推入队列,例如12,12-2*2=8,同时经过12-1*1-1*1-1*1-1*1=8,这两种情况,12-2*2肯定是为首选。
        Set<Integer> visited = new HashSet<>();
        visited.add(n);

        int[] step = new int[n+1];  //从起始值到达某值的步数
        step[n] = 0;

        while(!deque.isEmpty()){
            int num = deque.poll();
            int s = step[num];
            for(int i = 0 ; ;i++){
                int a = num - i * i;
                if(a < 0 ) break;
                if(a == 0) return step[num] + 1;
                if(!visited.contains(a)){
                    deque.offer(a);
                    visited.add(a);
                    step[a] = s + 1;
                }
            }
        }
        return -1;       
    }
}
  • 方法3:基于广度优先遍历(在树中也为层次遍历)。因为题目中要求得出完全平方数的个数最少,因此类似于最短路径。通过广度优先遍历,首先使得next等于0,即求出解。
class Solution {
	public int numSquares(int n) {
        Deque<Integer> deque = new LinkedList<>();
        HashSet<Integer> set = new HashSet<>();
        int level = 0;
        deque.offer(n);
        while(!deque.isEmpty()){
            level++;
            int curSize = deque.size();
            for(int i = 0 ; i < curSize ; i++){
                int curNum = deque.poll();
                for(int j = 1 ; curNum - j * j >= 0 ;j++){
                    int next = curNum - j * j;
                    if(next == 0 ){
                        return level;
                    }
                    if(!set.contains(next)){
                        set.add(next);
                        deque.offer(next);
                    }
                }
            }
        }
        return -1;
    }
}
  • 方法4:记忆化搜索+回溯。
class Solution {
    Integer[] memo;
    public int numSquares(int n) {
        this.memo = new Integer[n+1];
        return dfs(n);
    }

    //返回值为count,也就是完全平方数的数量。放在树中来理解,也就是树的层数。
    public int dfs(int n){        
        if(n == 0 ){
            return 0;
        }
        
        if(memo[n] != null){
            return memo[n];
        }

        int count = Integer.MAX_VALUE;
        
        for(int i = 1;n-i*i>=0 ;i++){
            count = Math.min(count,dfs(n-i*i) + 1);
        }
        return memo[n] = count;
    }
}
  • 注:在方法1与方法2的对比中。我们可以发现,通过数组来存储当前节点的访问标记会使节点更为优化,因此节点都为数字时,可通过数字对应的索引来确定是否访问标记。这样做,时间消耗更为少一些。

127.单词接龙

  • 方法1:单向广度优先遍历。
class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        
        Set<String> set = new HashSet<>(wordList);
        set.remove(beginWord);

        Set<String> visited = new HashSet<>();
        visited.add(beginWord);

        Deque<String> deque = new LinkedList<>();
        deque.offer(beginWord);

        int step = 1;

        while(!deque.isEmpty()){
            int curSize = deque.size();
            for(int i = 0 ; i < curSize ; i++){
                String curStr = deque.poll();
                char[] arrayStr = curStr.toCharArray();

                for(int j = 0 ; j < arrayStr.length ; j++){
                    char ch = arrayStr[j];

                    for(char k = 'a' ; k <= 'z';k++){
                        if(k == ch ){
                            continue;
                        }
                        arrayStr[j] = k;
                        String compareStr = String.valueOf(arrayStr);
                        if(set.contains(compareStr)){
                            if(compareStr.equals(endWord)){
                                return step+1;
                            }
                            if(!visited.contains(compareStr)){
                                deque.offer(compareStr);
                                visited.add(compareStr);
                            }
                        } 
                    }
                    arrayStr[j] = ch;
                }
            }
            step++;
        }
        return 0;
    }
}
  • 方法2:双向广度优先遍历。通过beginSet与endSet往复遍历,若beginSet遍历的结果存在于endSet中,则双向打通。
class Solution {
    //双向广度优先遍历
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Set<String> wordSet = new HashSet<>(wordList);
        //必须存在的判断,因为题目中表明,endword不在字典中则输出0
        if(wordSet.size() == 0 || !wordSet.contains(endWord)){
            return 0;
        }

        Set<String> beginSet = new HashSet<>();
        beginSet.add(beginWord);

        Set<String> endSet = new HashSet<>();
        endSet.add(endWord);

        Set<String> visited = new HashSet<>();
        visited.add(beginWord);
        visited.add(endWord);

        int step = 1;

        while(!beginSet.isEmpty() && !endSet.isEmpty()){
            
            //交换,每次前进数据量小的那一部分,时间上的小优化
            if(beginSet.size() > endSet.size()){
                Set<String> temp = endSet;
                endSet = beginSet;
                beginSet = temp;
            }
            
            Set<String> nextSet = new HashSet<>();

            for(String arrayStr : beginSet){
                char[] charStr = arrayStr.toCharArray();

                for(int j = 0 ; j < charStr.length; j++){
                    char ch = charStr[j];
                    for(char k = 'a' ; k <= 'z' ; k++){
                        if(ch == k){
                            continue;
                        } 
                        charStr[j] = k;
                        String comStr = String.valueOf(charStr);
                        if(wordSet.contains(comStr)){
                            if(endSet.contains(comStr)){
                                return step + 1;
                            }
                            if(!visited.contains(comStr)){
                                nextSet.add(comStr);
                                visited.add(comStr);
                            }
                        }
                    }
                    charStr[j] = ch;
                }
            }   
            beginSet = nextSet;
            step++;
        }
    return 0;
    }
}

347.前K个高频元素

  • 方法1:基于优先队列。通过HashSet来存储相应键值对对应的信息。通过PriorityQueue定义比较器,来使队列中按一定数据排序。
class Solution {

   //使用优先队列,底层基于堆排序。  有两种方式,详情看提交答案版
   public int[] topKFrequent(int[] nums, int k) {
       Map<Integer,Integer> map = new HashMap<>();
       for(int num : nums){
           if(map.containsKey(num)){
               map.put(num, map.get(num) + 1);
           }else{
               map.put(num,1);
           }
       }

       //定义优先队列
       Queue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>(){
           @Override
           public int compare(Integer a ,Integer b){
               return map.get(a) - map.get(b);
           }
       });

       for(Integer key : map.keySet()){
           queue.offer(key);
           //此步可省略,因为在存入数组的相应操作中只取前K个,频繁的进行出队会使时间消耗变多。
           if(queue.size() > k){
               queue.poll();
           }
       }

       int[] arr = new int[k];
       for(int i  = 0 ; i < k ;i++){
           arr[i] = (queue.poll());
       }
       return arr;
   }
}
  • 方法2:基于桶排序。将数字出现的频率存入对应的“桶”中,最终从最大的“桶”开始遍历,依次输出的数值就为出现次数最多的值。
class Solution {

//使用桶排序
   public int[] topKFrequent(int[] nums, int k) {
       Map<Integer,Integer> map = new HashMap<>();
       //存储出现的频率
       List<Integer>[] list = new List[nums.length+1];
       for(int num : nums){
           if(map.containsKey(num)){
               map.put(num, map.get(num) + 1);
           }else{
               map.put(num,1);
           }
       }

       for(int key : map.keySet()){
           int i = map.get(key);
           if(list[i] == null){
              list[i] = new ArrayList();
           } 
           list[i].add(key);
       }

       int result[] = new int[k];
       int index = 0;
       System.out.println(Arrays.toString(list));
       for(int i = list.length - 1 ; i >= 0 && index < k; i--){
           if(list[i] != null){
               for(Integer a : list[i]){
                   result[index++] = a;
               }
           }
       }
       return result;
   }
}
  • 方法3:在方法2中有一些优化。
 class Solution {
	public int[] topKFrequent(int[] nums, int k) {
        List<Integer> res = new ArrayList();
        // 使用字典,统计每个元素出现的次数,元素为键,元素出现的次数为值
        HashMap<Integer,Integer> map = new HashMap();
        for(int num : nums){
            if (map.containsKey(num)) {
               map.put(num, map.get(num) + 1);
             } else {
                map.put(num, 1);
             }
        }

        //桶排序
        //将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标
        List<Integer>[] list = new List[nums.length+1];
        for(int key : map.keySet()){
            // 获取出现的次数作为下标
            int i = map.get(key);
            if(list[i] == null){
               list[i] = new ArrayList();
            } 
            list[i].add(key);
        }
        
        // 倒序遍历数组获取出现顺序从大到小的排列
        for(int i = list.length - 1;i >= 0 && res.size() < k;i--){
            if(list[i] == null) continue;
            //将对应的list一次性压入
            res.addAll(list[i]);
        }
        //在方法2中,每次碰到一个list就遍历所有的数值,此举动较为消耗时间。在此方法中,遍历只存在一次。
        int[] result = new int[k];
        for(int i = 0 ; i < res.size();i++){
            result[i] = res.get(i);
        }
        return result;
    }
}

桶排序的应用场景

桶排序的适用场景非常明了,那就是在数据分布相对比较均匀或者数据跨度范围并不是很大时,排序的速度还是相当快 且简单的。
但是当数据跨度过大时,这个空间消耗就会很大;如果数值的范围特别大,那么对空间消耗的代价肯定也是不切实际 的,所以这个算法还有一定的局限性。同样,由于时间复杂度为 O(n+m),如果 m 比 n 大太多,则从时间上来说,性能也并不是很好。
但是实际上在使用桶排序的过程中,我们会使用类似散列表的方式去实现,这时的空间利用率会高很多,同时时间复杂会有一定的提升,但是效率还不错。
我们在开发过程中,除了对一些要求特别高并且数据分布较为均匀的情况使用桶排序,还是很少使用桶排序的,所以即使桶排序很简单、很快,我们也很少使用它。
桶排序更多地被用于一些特定的环境,比如数据范围较为局限或者有一些特定的要求,比如需要通过哈希映射快速获取某些值、需要统计每个数的数量。但是这一切都需要确认数据的范围,如果范围太大,就需要巧妙地解决这个问题或者使用其他算法了。

23.合并K个升序链表

  • 方法1:基于优先队列
class Solution {
    //优先队列
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists == null || lists.length == 0){
            return null;
        }
        Queue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>(){
            @Override
            public int compare(ListNode a , ListNode b ){
                return a.val - b.val;
            }
        });
        ListNode sentry = new ListNode(0);
        ListNode pre = sentry;
        for(int i = 0 ; i < lists.length; i++){
            while(lists[i] != null){
                queue.offer(lists[i]);
                lists[i] = lists[i].next;
            }
        }
        while(!queue.isEmpty()){
            pre.next = queue.poll();
            pre = pre.next;
        }
        pre.next = null;
        return sentry.next;
    }
}
  • 方法2:分治法。两两合并,最终合并为一个链表。
class Solution {
	public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0 || lists == null){
            return null;
        }
        return merge(lists,0,lists.length -1);
    }

    public ListNode merge(ListNode[] lists, int low ,int high){
        if(low == high) return lists[low];        
        int mid = (low + high) / 2;
        ListNode left =  merge(lists,low,mid);
        ListNode right =  merge(lists,mid+1,high);
        return mergeList(left,right);
    }

    public ListNode mergeList(ListNode l1 ,ListNode l2){
        if(l1 == null) return l2;
        if(l2 == null) return l1;
        if(l1.val < l2.val){
            l1.next = mergeList(l1.next,l2);
            return l1;
        } 
        l2.next = mergeList(l1,l2.next);
        return l2;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值