刷题小笔记

1.常用函数

  1. Math
求幂:Math.pow(3, a)3的a次方
求绝对值:Math.ads()
获取int的最大值:Integer.MAX_VALUE
  1. 思想
向下整除 n/2等价于 右移一位 n>>1;
取余数 n%2n等价于 判断二进制最右位 n&1;
取下标start与end之间的中间下标:int mid = start+(end-start)/2 这样可以防止溢出
byteshortintlong类型定义的数组,初始化默认是0
  1. String
String.trim():去掉字符串前面和后面所有的空格
StringBuilder.toString();转换为字符串
String.valueOf(int i) 将整型转换为字符串
s.substring(begin,end) 字符串截取,包左不包右
s.toCharArray()转换为字符数组
  1. map
map.getOrDefault(key,value1)//如果key已经存在,则获取到对应的value;否则就将该key对应的就是value1
map.containsKey(key)
  1. Arrays
  2. 队列,栈
队列:
声明:(二者方法用起来差不多)
Deque <Integer> deque =new LinkedList<>();
Deque <Integer> deque =new ArrayDeque<>();
deque.peek() 返回队首的值,不弹出
deque.poll() 返回队首的值,弹出
deque.removeFirst() 返回头结点,弹出
deque.offerLast(x) 添加到队尾,超出长度返回false
deque.offerFirst(x) 添加到队首,超出长度返回false
deque.add(x) 添加到队尾,超出长度抛异常
栈:
声明:
LinkedList<Integer> stack = LinkedList<>();
stack.push(x) 添加
stack.pop(x)获取并删除,出栈
stack.peek(x)获取不删除

//PriorityQueue是Java默认的小顶堆,重写比较器可以构造大顶堆
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() {
    public int compare(Integer num1, Integer num2) {
         return num2 - num1;
    }
});

2.二叉树

  • 一些注意点
1.层序遍历用的时队列,中序等等用的是栈
  1. 二叉树中两个结点的最近公共祖先
 class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null || root == p || root == q) return root; //终止条件
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        if(left == null) return right; //p,q都不在左子树中
        if(right == null) return left;//p,q都不在右子树中
        return root;
    }
}
  1. 二叉树完全性的验证
//层序遍历,设置一个停止标志,遇到空节点,停止标志为真,如果停止标志为真,再遍历遇到非空节点,则非完全二叉树。
public boolean isCompleteTree(TreeNode root) {
        if(root==null)
            return true;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        boolean flag = false;
        while(!queue.isEmpty()){
            TreeNode temp = queue.poll();
            //continue 语句是跳过循环体中剩余的语句而强制执行下一次循环,其作用为结束本次循环
            if(temp==null){
                flag=true; //标记出现了空结点,后续再出现非空,则证明是非完全的二叉树
                continue;
            }
            if(flag)
                //如果还能进循环,证明queuequeue非空,证明还是有元素的,也就是出现了空之后又出现非空
                return false;
            queue.add(temp.left);
            queue.add(temp.right);
        }
        return true;
}
  1. 二叉树中的最大路径和
 class Solution {
    int maxSum = Integer.MIN_VALUE; //全局变量用于保存最后的最大值
    public int maxPathSum(TreeNode root) {
        //maxGain()用于计算每个结点的最大贡献值:
        //叶子节点贡献值=本身值,非叶子节点贡献值=本身值+max(左子树贡献,右子树贡献)————递归
        //在计算贡献值的同时更新最大值
        maxGain(root); 
        return maxSum;
    }

    public int maxGain(TreeNode node) {
        if (node == null) {
            return 0;
        }
        
        // 递归计算左右子节点的最大贡献值
        // 只有在最大贡献值大于 0 时,才会选取对应子节点
        int leftGain = Math.max(maxGain(node.left), 0);
        int rightGain = Math.max(maxGain(node.right), 0);

        // 该节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
        int priceNewpath = node.val + leftGain + rightGain;

        // 更新答案
        maxSum = Math.max(maxSum, priceNewpath);

        // 返回节点的最大贡献值
        return node.val + Math.max(leftGain, rightGain);
    }
}
  1. 判断二叉树是否对称
//这种另外定义一个递归方法的也很常见,注意啦
class Solution {
    public boolean isSymmetric(TreeNode root) {
        if(root==null)
            return true;
        return helper(root.left,root.right);
    }

    private boolean helper(TreeNode left,TreeNode right){
        if(left==null&&right==null)
            return true;
        if(left==null||right==null)
            return false;
        return left.val==right.val&&helper(left.left,right.right)&&helper(left.right,right.left);
    }
}
  1. 将有序数组转换为二叉搜索树
//把握好递归的条件!递归的构建!
public TreeNode sortedArrayToBST(int[] nums) {
        int len = nums.length;
        return bfs(nums,0,len-1); 
    }
public TreeNode bfs(int[] nums,int start,int end){
        if(start>end)
            return null;
        int mid = start+(end-start)/2;
        TreeNode root  = new TreeNode(nums[mid]);
        root.left = bfs(nums,start,mid-1);
        root.right = bfs(nums,mid+1,end);
        return root;
}

3. 链表

*有几个固定套路:

(1)方便对头节点的操作,创建哑节点dummyhead(这一步看题里具体的构造函数)
ListNode dummyhead = new ListNode(1);
dummyhead .next=head;
(2)cur = dummyhead, cur指向当前节点
(3)cur = cur->next,进行链表遍历
(4)最后返回:return dummyhead.next;

  1. 反转链表
public static ListNode reverseListIterative(ListNode head) {
    ListNode prev = null; //前指针节点
    ListNode curr = head; //当前指针节点
    //每次循环,都将当前节点指向它前面的节点,然后当前节点和前节点后移
    while (curr != null) {
        ListNode nextTemp = curr.next; //临时节点,暂存当前节点的下一节点,用于后移
        curr.next = prev; //将当前节点指向它前面的节点
        prev = curr; //前指针后移
        curr = nextTemp; //当前指针后移
    }
    return prev;
}
  1. 合并两个有序链表
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode prehead = new ListNode(-1);
        //prehead作为虚拟头节点,用于最后的返回
        //prev用于构建链表
        ListNode prev = prehead;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                prev.next = l1;
                l1 = l1.next;
            } else {
                prev.next = l2;
                l2 = l2.next;
            }
            prev = prev.next;
        }

        // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
        prev.next = l1 == null ? l2 : l1;

        return prehead.next;
    }
  1. 删除倒数第n个结点
//快慢指针
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        ListNode first = head;
        ListNode second = dummy;//因为找的是待删除结点的前一个结点,所以送dummy开始
        for (int i = 0; i < n; ++i) {
            first = first.next;
        }
        while (first != null) {
            first = first.next;
            second = second.next;
        }
        second.next = second.next.next;
        ListNode ans = dummy.next;//不写头节点是因为,头节点可能已经被删了
        return ans;
    }
}
  1. 删除指定值的节点
public ListNode deleteNode(ListNode head, int val) {
        if(head.val == val) return head.next;
        ListNode pre = head, cur = head.next;
        while(cur != null && cur.val != val) {//去找该结点
            pre = cur;
            cur = cur.next;
        }
        if(cur != null) 
        	pre.next = cur.next; //执行删除操作
        return head;
}
  1. 链表的中间节点
//(1)完全可以进行两次循环,一次算链表长度count,另一次for到循环i<count/2
//(2)此处方法——神奇的快慢指针:slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间
public ListNode middleNode(ListNode head) {
        ListNode slow = head, fast = head;
        //这里的边界条件需要注意:(奇数长度时没有区别)
        //如果fast != null && fast.next != null,返回的是偶数的两个中间结点的后一个
        //如果fast != null && fast.next != null,返回的是偶数的两个中间结点的前一个
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }
  1. 环形链表:给定一个链表,判断链表中是否有环。
//依旧是神奇的快慢指针,有环的话快的必定可以通过环追上慢的。注意返回条件的判定。
public boolean hasCycle(ListNode head) {
        if(head==null||head.next==null)
            return false;
        ListNode slow = head;
        ListNode fast = head.next;//为了让循环进行起来
        while (fast!=slow){
            if(fast==null||fast.next==null)
                return false;
            fast=fast.next.next;
            slow=slow.next;
        }
        return true;
    }
  1. 环形链表2:给定一个链表,判断链表中是否有环,有环就返回环的第一个结点。
/*依旧是快慢指针。
假设:从头节点到环入口前一个结点一共数量为a
完整环的节点数为b
那么:
    走a+nb步一定是在环入口
    第一次相遇时慢指针已经走了nb步
*/
public ListNode detectCycle(ListNode head) {
        ListNode fast = head, slow = head;
        while (true) {
            if (fast == null || fast.next == null) return null;
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) 
            	break;//此时走了nb步
        }
        fast = head;//用于计算a步,再次相遇就是环的入口啦
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
  1. 寻找数组中的重复数

/*
使用环形链表II的方法解题,的关键是要理解如何将输入的数组看作为链表。
如数组:[1,3,4,2,2],将下角标和元素值对应,有:
0->1
1->3
2->4
3->2
4->2
这是就可以看成链表:0-1-3-2-4-2, 2-4-2就是那个环,2是环的起点,也是要找的重复元素
*/
public int findDuplicate(int[] nums) {
        int slow=0;
        int fast=0;
        slow=nums[slow];//相当于向前走一步 slow=slow.next
        fast=nums[nums[fast]];//相当于向前走两步 fast=fast.next.next

        while(slow!=fast){
            slow=nums[slow];//相当于向前走一步
            fast=nums[nums[fast]];//相当于向前走两步
        }
        int pre1=0;
        int pre2=slow;
        while(pre1 != pre2){
            pre1 = nums[pre1];//表示只走一步
            pre2 = nums[pre2];
        }
        return pre1;
    }
    
  1. LRU缓存机制
//自己实现定义双向链表,包含虚拟表头、表尾。结合hashmap实现完成。具体要求看题目吧,看起来还挺能考
public class LRUCache {
    //定义双向链表类,用于存数据
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
    }

    //利用hashmap保存key,node,便于查找和添加
    //添加、删除时,都要注意双向链表和hash表都要操作
    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;//实际元素数量
    private int capacity;//容量
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项.重要!容易忘。
                cache.remove(tail.key);
                --size;
            }
        }
        else {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}
 
public ListNode insertionSortList(ListNode head) {
    //每次看一个元素,从前面已经排好序的一段链表中找到该元素的插入位置
    ListNode dummy = new ListNode(0);
    dummy.next=head;
    ListNode cur= head.next;
    ListNode lastIn = head;
    ListNode pre;
    
    while(cur!=null){
        if(cur.val<lastIn.val){//从链表头开始,寻找插入位置
            pre=dummy;
            while(pre.next.val<=cur.val)  //找到要插入位置的前一个元素。因为找前一个,所以是用pre.next.val进行比较       
                pre=pre.next;
            
            lastIn.next=cur.next;//插入。注意这个写法。
            cur.next=pre.next;
            pre.next=cur;
            
        }
        else
            lastIn=lastIn.next;    
        cur=lastIn.next;        
    }
    return dummy.next;
}

4. 数组

  1. 在有序数组中,一个数出现的次数
    附上二分法的不同边界判断方法
//这里helper定义的就是找出比target大的第一个元素的下标
//[5,7,7,8,8,10], target = 8,那helper(nums, target)返回的就是10对应的下角标
//target - 1=7,helper(nums, target - 1)返回的就是第一个8对应的下角标
//二者相减,就是8的个数
class Solution {
    public int search(int[] nums, int target) {
        return helper(nums, target) - helper(nums, target - 1);
    }
    int helper(int[] nums, int tar) {
        int i = 0, j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(tar<nums[m])
                j=m-1;
            else 
                i = m + 1;
        }
        return i;
    }
}

4. 栈,堆

  1. 检查括号是否有效
public boolean isValid(String s) {
        Map<Character,Character> map = new HashMap<>();
        map.put('(',')');
        map.put('{','}');
        map.put('[',']');
        map.put('?','?');//防止栈空时pop报错
        LinkedList<Character> stack = new LinkedList<>();
        stack.offerLast('?');//防止栈空时pop报错
        if(s.length()>0&&!map.containsKey(s.charAt(0)))
            return false;//第一个就是右半部分括号,返回false
        for(Character c:s.toCharArray()){            
            if(map.containsKey(c))
                stack.offerLast(c);//左半括号进栈
            else if(map.get(stack.removeLast())!=c){//读到右半括号的情况
                return false;
            }
        }
        return stack.size()==1;
    }
  1. 括号生成
//充分使用回溯。
//对于回溯的理解:
List<String> res = new ArrayList<>();
        public List<String> generateParenthesis(int n) {
            if(n <= 0){
                return res;
            }
            getParenthesis("",n,n);
            return res;
        }

private void getParenthesis(String str,int left, int right) {
            if(left == 0 && right == 0 ){
                res.add(str);
                return;
            }
            if(left == right){
                //剩余左右括号数相等,下一个只能用左括号
                getParenthesis(str+"(",left-1,right);
            }else if(left < right){
                //剩余左括号小于右括号,下一个可以用左括号也可以用右括号
                if(left > 0){
                    getParenthesis(str+"(",left-1,right);
                }
                getParenthesis(str+")",left,right-1);
            }
}

5. 字符串

  1. 反转字符串中单词的顺序并去掉空格
 class Solution {
    public String reverseWords(String s) {
        if (s == null) return null;
        char[] s_arr = s.toCharArray();
        int n = s_arr.length;
        // 翻转这个数组
        reverse(s_arr, 0, n - 1);
        // 翻转每个单词
        word_reverse(s_arr, n);
        // 去除多余空格
        return clean_space(s_arr, n);
    }

    private void reverse(char[] s_arr,int start,int end){
        while(start<end){
            char ch = s_arr[start];
            s_arr[start++]=s_arr[end];
            s_arr[end--]=ch;
        }
    }

    private void word_reverse(char[] s_arr,int n){
        //找到每个单词的首尾位置后,调用reverse
        int i =0;
        int j = 0;
        while(j<n){
            while(i<n&&s_arr[i]==' ')
                i++;//去除空格的影响,找到实际首字母
            j = i;
            while(j<n&&s_arr[j]!=' ')
                j++;
            reverse(s_arr,i,j-1);
            i=j;//这个单词结束的位置,经过循环继续寻找下个单词
        }
        
    }

    private String clean_space(char[] s_arr,int n){
        int i =0;
        int j=0;
        while(j<n){
            while(j<n&&s_arr[j]==' ')
                j++;//去除空格
            while(j<n&&s_arr[j]!=' ')
                s_arr[i++]=s_arr[j++]; //复制单词
            while(j<n&&s_arr[j]==' ')
                j++;//去单词后面的空格
            if(j<n)
                s_arr[i++]=' ';//证明斌并非最后一个单词,需要拼接一个空格
        }
        return new String(s_arr).substring(0,i);
    }
}
  1. 最长不含重复字符的字符串
//动态规划,使用dp[j]保存当前位置对应的最长长度
//i,j分别表示当前字符上一次出现、这一次出现的位置
//状态转移:dp[j]<j-i:dp[j] = dp[j-1]+1
//			dp[j]>j-i:dp[j] = j-i
public int lengthOfLongestSubstring(String s) {
        int res = 0;//保存最后结果
        int temp=0;//保存中间结果
        for(int j=0;j<s.length();j++){
            int i = j-1;
            while(i>=0&&s.charAt(i)!=s.charAt(j))
                i--;//找到与当前字符重复的上一个位置
            temp=temp<j-i?temp+1:j-i;//状态转移的过程
            res = Math.max(res,temp); 
        }
        return res;
}
________________用哈希表找i的方法_______________________
 Map<Character, Integer> dic = new HashMap<>();
 for(int j = 0; j < s.length(); j++) {
     int i = dic.getOrDefault(s.charAt(j), -1); // 获取索引 i
      dic.put(s.charAt(j), j); // 更新哈希表
      temp=temp<j-i?temp+1:j-i;//状态转移的过程
      res = Math.max(res,temp); 
 }
  1. 最长回文子串
//中心扩散法。如果a为回文串,则左右同时扩散相同的字符,得到的串依旧是回文串
//特殊情况:当只有一个字符时,必是回文串;当有两个字符时,二者相等的情况下才是回文串
public String longestPalindrome(String s) {
        if(s==null||s.length()==0)
            return "";
        int start =0;//保存最终要返回的起始位置
        int end = 0;//保存最终要返回的终止位置
        for(int i = 0;i<s.length();i++){
            int len1=expandAroundCenter(s,i,i);//考虑长度为1或者奇数长度的回文串情况
            int len2=expandAroundCenter(s,i,i+1);//考虑长度为2或者偶数长度的回文串情况
            int len=Math.max(len1,len2);
            if(len>end-start){
                start=i-(len-1)/2;
                end=i+len/2;
            }
        }
        return s.substring(start,end+1);
    }

    public int expandAroundCenter(String s, int i,int j){
        while(i>=0&&j<s.length()&&s.charAt(i)==s.charAt(j)){
            i--;
            j++;
        }
        //回文串长度为j-i+2-1
        return j-i-1;
    }
  1. 最长公共子串
public String longestCommonPrefix(String[] strs) {
        if(strs==null||strs.length==0)
            return "";
        String res=strs[0];//用字符串数组的第一个元素初始化结果串
        for(int i=1;i<strs.length;i++){//后面每一个元素都与结果串依次匹配寻找公共子串
            int j;
            for(j=0;j<strs[i].length()&&j<res.length();j++){
                if(res.charAt(j)!=strs[i].charAt(j))
                    break;
            }
            if(j==0)
                return "";
            res=res.substring(0,j);//更新公共子串
        }
        return res;
    }
  1. Z字形变换
//按顺序遍历字符串 s 时,每个字符 c 在 Z字形中对应的 行索引 先从 s1增大至 sn,再从 sn减小至 s1 …… 如此反复。
public String convert(String s, int numRows) {
        if(numRows<2)
            return s;
        List<StringBuffer> rows = new ArrayList<>();
        for(int i=0;i<numRows;i++)
            rows.add(new StringBuffer());
        
        int i = 0, flag = -1;//字符从1到n,再从n到1,利用flag进行方向的转变(+1或-1)
        for(char c : s.toCharArray()) {
            rows.get(i).append(c);
            if(i == 0 || i == numRows -1) flag = - flag;//方向转换
            i += flag;
        }
        StringBuffer res = new StringBuffer();
        for(int j = 0;j<numRows;j++)
            res.append(rows.get(j));
        return res.toString();
    }

6. 其他常考问题

*岛屿问题总结与分析(深度好文)

  1. 基本问题:岛屿的数量

//记得看链接,是详细解读

  1. Pow(x,n)
public double myPow(double x, int n) {
    if(x==0.0f)
        return 0.0d;//0的任何指数幂都是0
    long b = n; //用于处理int n越界的问题
    double res=1.0d;

    if(b<0){//可能出现越界情况时,就赋值给long,以后已知用它操作
        x=1/x;
        b=-b;
    }

    while(b>0){
        if((b&1)==1)//相当于b%2==1
            res*=x; //奇数的话就存进res
        x*=x; //x变x平方
        b>>=1; //b向下整除2
    }
    return res;
}
  1. 判断回文数字
public boolean isPalindrome(int x) {
        // 特殊情况:
        // 如上所述,当 x < 0 时,x 不是回文数。
        // 同样地,如果数字的最后一位是 0,为了使该数字为回文,
        // 则其第一位数字也应该是 0
        // 只有 0 满足这一属性
        if (x < 0 || (x % 10 == 0 && x != 0)) {
            return false;
        }

        int revertedNumber = 0;
        while (x > revertedNumber) {//获取后一半数字的反转数字
            revertedNumber = revertedNumber * 10 + x % 10;
            x /= 10;
        }

        // 当数字长度为奇数时,我们可以通过 revertedNumber/10 去除处于中位的数字。
        // 例如,当输入为 12321 时,在 while 循环的末尾我们可以得到 x = 12,revertedNumber = 123,
        // 由于处于中位的数字不影响回文(它总是与自己相等),所以我们可以简单地将其去除。
        return x == revertedNumber || x == revertedNumber / 10;
    }
  1. 字母异位分组
//字母异位词指字母相同,但排列不同的字符串。
//先排序,然后将排序后的结果作为key,存入map中
public List<List<String>> groupAnagrams(String[] strs) {
    if(strs==null||strs.length==0)
        return new ArrayList<List<String>>();
    Map<String,List<String>> map = new HashMap<>();
    for(String str:strs){
        char[] ch = str.toCharArray();
        Arrays.sort(ch);
        String key = new String(ch);
        List<String> list = map.getOrDefault(key,new ArrayList<String>());
        list.add(str);
        map.put(key,list);
    }
    //这种方式妙哦
    return new ArrayList<List<String>>(map.values());      
}
  1. 最大公约数(好未来)
public sta
int a,int b;

  1. 最小公倍数(好未来)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值