剑指Offer系列——每日一题(持续更新)


算法题:就是确定需要使用哪种遍历算法、哪个数据结构来解题。

简单算法

03、数组中重复的数字(利用数组索引和值一对一的特性)

【一个长度为n的数组nums、里面的所有数字范围在 0~n-1内】
但有些数字是重复的。却不知道哪些重复、请找出任意一个重复数字

解法一:利用Set集合的唯一性(时间复杂度O(n) 空间复杂度O(n))

public int findRepeatNumber(int[] nums) {
        Set<Integer> dic = new HashSet<>();
        for(int num : nums) {
            if(dic.contains(num)) return num;
            dic.add(num);
        }
        return -1;
    }

解法二:原地交换(需要数组中值的范围0~n-1在数组长度n内)(时间复杂度O(n) 空间复杂度O(1))

该题数组中的下标和值不是一一对应的。而是一对多的关系。
因此可以将值放在该值对应的索引处、如果交换时该位置已经有值、则代表重复了
利用索引和值的一一对应性

    private static int findRepeatNumber(int[] nums){
        int i = 0, n = nums.length;
        while (i < n){
            // 如果该值对应的索引就是他自己
            if (nums[i] == i){
                i++;
                continue;
            }
            int temp = nums[i];
            // 如果该值对应的索引处已经调整过值
            if(nums[temp] == temp){
                return nums[i];
            }
            // 否则、交换值
            nums[i] = nums[temp];
            nums[temp] = temp;
        }
        return -1;
    }

05、替换空格【String类不可变】

将给定字符串中的空格、替换为%20、

    private String replaceBlank(String s){
    // Java中String类不可变、如果在String上替换、每次都会新建一个String对象。
    //  因此使用StringBuilder 来操作
        StringBuilder res = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (c == ' '){
                res.append("%20");
            }else {
                res.append(c);
            }
        }
        return res.toString();
    }

06、从尾到头打印链表【递归、栈】

给你一个单向链表、用数组按反着的顺序输出链表的值

    private static List<Integer> res = new ArrayList<>();
    private static int[] printReverseListNode(ListNode head){
        print(head);
        int[] result = new int[res.size()];
        for (int i = 0; i < res.size(); i++) {
            result[i] = res.get(i);
        }
        return result;
    }

    private static void print(ListNode head){
        // 递归到链表末尾(递归出口):返回
        if (head == null){
            return;
        }
        // 递归
        print(head.next);
        // 递归到末尾返回后、走到这一步、add末尾结点的值
        res.add(head.val);
    }

08、双栈实现队列

用两个栈实现队列的尾插和头删两个方法

class StackImplQueue{
    private Stack<Integer> stack1;
    private Stack<Integer> stack2;
    public StackImplQueue(){
        this.stack1 = new Stack<>();
        this.stack2 = new Stack<>();
    }

    public void appendTail(int e){
        stack1.push(e);
    }

    public int deleteHead(){
        while (stack1 != null && stack1.size() > 0){
            stack2.push(stack1.pop());
        }
        Integer tem = stack2.pop();
        System.out.println("头元素:" + tem + ",删除成功!");
        return tem;
    }

    public void outPut(){
        while (stack1 != null && stack1.size() > 0){
            stack2.push(stack1.pop());
        }
        StringBuilder sb = new StringBuilder();
        int[] temp = new int[stack2.size()];
        int i = 0;
        while (stack2 != null && stack2.size() > 0){
            Integer num = stack2.pop();
            sb.append(num);
            temp[i++] = num;
        }
        System.out.println(sb);
        for (int e : temp) {
            stack1.push(e);
        }
    }
}

10、斐波那契数列(青蛙跳台阶)

写一个函数、输入n,求斐波那契数列的第n项
斐波那契数列的定义:F(0) = 0,F(1) = 1,F(N) = F(N - 1) + F(N - 2)
为保证结果不超过整型上限、要将结果对1000000007取模

解法一(递归)

    private static int getFibonacci(int n){
        if (n < 3){
            return n;
        }
        return (getFibonacci(n - 1) + getFibonacci(n - 2)) % 1000000007;
    }

解法二(动态规划)

    private static int getFibonacci(int n){
        int a = 0, b = 1, sum = 0;
        for (int i = 0; i < n; i++) {
            sum = (a + b) % 1000000007;
            a = b;
            b = sum;
        }
        return sum;
    }

11、旋转数组的最小值

把一个数组最开始的若干个元素放到数组末尾、称为数组的旋转。
现在:输入一个递增排序的数组的旋转、请输出其旋转数组的最小值。
即使是旋转数组、因为原数组是递增的、也可以用二分查找来提升效率
接下来就是判断:每次二分该往左还是往右查找
几种情况:2,2,2,0,1、5,6,0,1,2,3,4、1,2,0,0,0、2,3,0,1,1

    private static int minNumInSpinArray(int[] nums){
        int len = nums.length;
        if (len == 0){
            return -1;
        }
        if (len == 1){
            return nums[0];
        }
        int left = 0, right = len - 1, min = Integer.MAX_VALUE;
        while (left < right){
            int mid = (left + right) / 2;
            //根据枚举的几种情况、可以得出以下判断条件
            if (nums[mid] >= nums[right]){
                left = mid + 1;
            }else {
                right = mid - 1;
            }
            min = Math.min(min, nums[mid]);
        }
        return min;
    }

15、汉明重量(计算二进制数中1的个数)

n & ( n - 1 ) <==> 将二进制数n最低一位的0 变为1

public int hammingWeight(int n) {
        int ret = 0;
        while (n != 0) {
            n &= n - 1;
            ret++;
        }
        return ret;
    }

21、调整数组顺序【快慢指针】

将数组顺序调整成: 前面全是奇数、后面全是偶数。奇数/偶数部分不用排序

    private static int[] printNumUntilN(int[] nums) {
        int slow = 0, fast = 0;
        int len = nums.length;
        while (slow < len) {
            // 如果slow是偶数
            if (nums[slow] % 2 == 0) {
                while (fast < len) {
                    // 如果找到一个奇数、就换位
                    if (nums[fast] % 2 == 1) {
                        int temp = nums[fast];
                        nums[fast] = nums[slow];
                        nums[slow] = temp;
                        // 如果slow后面全是偶数了、则已经满足题目要求、设置flag
                        fast = -1;
                        break;
                    }
                    fast++;
                }
                // 已经满足题目要求、提前结束循环。
                // 否则slow指针要一直遍历到末尾才会结束。
                if (fast != -1) {
                    return nums;
                }
            }
            slow++;
            // fast指针归位
            fast = slow;
        }
        return nums;
    }

22、链表倒数第K个节点

双指针L R、先移动R、直到L是R的倒数第K个时、LR一起移动。R.NEXT = NULL时返回

private static ListNode findKDesc(ListNode head, int k){
        ListNode left = new ListNode(0);
        left.next = head;
        ListNode right = left;
        while (right.next != null){
            right = right.next;
            if (k > 1){
                k--;
            }else {
                left = left.next;
            }
        }
        if (k > 1){
            return null;
        }
        return left;
    }

24、反转链表

方法一:非递归

    private static ListNode reverse(ListNode head) {
        ListNode tail = head;
        while (tail != null) {
            tail = tail.next;
        }
        // 前一节点
        ListNode newHead = null;
        ListNode next = null;
        while (head != tail) {
            // 先用temp变量暂存head.next节点(摘下 head的next指针)
            // 拿到头结点的下一个节点:next节点
            next = head.next;
            // 翻转:让head的指针指向newHead (第一轮时newHead=null)
            head.next = newHead;
            // 将本轮head赋给newHead(本轮的head就是新的newHead)
            newHead = head;
            // 将head指针后移一位(head = head.next 当前next设为新的head)
            head = next;
        }
        return newHead;
    }

方法二:递归

    public static ListNode reverseList(ListNode head) {
        return recur(head, null);    // 调用递归并返回
    }

    private static ListNode recur(ListNode cur, ListNode pre) {
        if (cur == null) { // 终止条件
            return pre; // pre为反转后的头结点、一直return回第一层
        }
        ListNode res = recur(cur.next, cur);  // 递归后继节点
        cur.next = pre;              // 反转指针
        return res;                  // 返回反转链表的头节点
    }

25、合并两个有序链表

    private static ListNode merge(ListNode head1, ListNode head2) {
        // 确保head1的头结点 < head2; 即head1的头结点为合并后的链表头
        if (head1.val > head2.val) {
            return merge(head2, head1);
        }
        ListNode pointer = head1;
        // 对链表1进行遍历、逐个与链表2比较。
        while (pointer.next != null) {
            // 如果此时head2==null、说明head2更短、已经全部合并到head1了、则合并结束、直接返回!
            if (head2 == null) {
                return head1;
            }
            // 小、则继续往下比
            if (pointer.next.val <= head2.val) {
                pointer = pointer.next;
            } else {
                // 大、则①将head2.next摘下(暂存) ②将head2接到head1中 ③将pointer指向head2
                ListNode temp = head2.next;
                head2.next = pointer.next;
                pointer.next = head2;
                head2 = temp; // 重置head2
            }
        }
        // 全部结束后、将head2剩余的元素直接接到head1尾部
        head2 != null ? pointer.next = head2 : pointer.next = null;
        return head1;
    }

26、树的子结构

判定树B是否是树A的子树

    public static void main(String[] args) {
        TreeNode rootA = new TreeNode(3);
        TreeNode a1 = new TreeNode(4);
        TreeNode a2 = new TreeNode(5);
        TreeNode a3 = new TreeNode(1);
        TreeNode a4 = new TreeNode(2);
        rootA.left = a1;
        rootA.right = a2;
        a1.left = a3;
        a1.right = a4;
        TreeNode rootB = new TreeNode(3);
        TreeNode b1 = new TreeNode(4);
        TreeNode b2 = new TreeNode(2);
        rootB.left = b1;
        b1.right = b2;
        System.out.println(isSonTree(rootA, rootB));
    }

    private static boolean isSonTree(TreeNode A, TreeNode B) {
        if (B == null) {
            return false;
        }
        // 先序遍历树A
        LinkedList<TreeNode> list = new LinkedList<>();
        list.add(A);
        while (list != null && list.size() > 0) {
            TreeNode treeNode = list.removeFirst();
            if (treeNode.left != null) {
                list.add(treeNode.left);
            }
            if (treeNode.right != null) {
                list.add(treeNode.right);
            }
            // 当树A的某个节点treeNode==B的根节点时。比较以treeNode为根的子树是否包含树B
            if (treeNode.value == B.value) {
                // 比较是否是子树
                return compare(treeNode, B);
            }
        }
        return false;
    }

    private static boolean compare(TreeNode A, TreeNode B) {
        if (B == null) {
            return true;
        }
        if (B != null && A != null && A.value == B.value) {
        // 左子树都相同、并且右子树都相同时,才符合判断
            return compare(A.left, B.left) && compare(A.right, B.right);
        }
        return false;
    }

28、判断二叉树是否对称

    public static void main(String[] args) {
        TreeNode rootA = new TreeNode(4);
        TreeNode a1 = new TreeNode(2);
        TreeNode a2 = new TreeNode(2);
        TreeNode a3 = new TreeNode(3);
        TreeNode a4 = new TreeNode(4);
        TreeNode a5 = new TreeNode(4);
        TreeNode a6 = new TreeNode(3);
        rootA.left = a1;
        rootA.right = a2;
        a1.left = a3;
        a1.right = a4;
        a2.left = a5;
        a2.right = a6;
        System.out.println(mirror(rootA));
    }

    private static boolean mirror(TreeNode root) {
        if (root == null) {
            return true;
        }
        return compare(root.left, root.right);
    }

    private static boolean compare(TreeNode A, TreeNode B) {
        if (A == null && B == null) {
            return true;
        }
        if (A == null || B == null) {
            return false;
        }
        return A.value == B.value ? compare(A.left, B.right) && compare(A.right, B.left) : false;
    }

29、顺时针打印矩阵

    public static void main(String[] args) {
        int[][] matrix = {new int[]{1, 2, 3, 4}, new int[]{5, 6, 7, 8}, new int[]{9, 10, 11, 12}};
        printClockwiseMatrix(matrix);
    }

    private static void printClockwiseMatrix(int[][] matrix) {
        boolean[][] walked = new boolean[matrix[0].length + 1][matrix.length + 1];
        StringBuilder sb = new StringBuilder();
        int i = 0, j = 0;
        sb.append(matrix[0][0]).append(" ");
        walked[0][0] = true;
        while (true) {
            int now = -1;
            if (j + 1 < matrix[0].length && !walked[i][j + 1]) {
                now = matrix[i][++j];
            } else if (i + 1 < matrix.length && !walked[i + 1][j]) {
                now = matrix[++i][j];
            } else if (j - 1 >= 0 && !walked[i][j - 1]) {
                now = matrix[i][--j];
            } else if (i - 1 >= 0 && !walked[i - 1][j]) {
                now = matrix[--i][j];
            }
            if (now != -1) {
                walked[i][j] = true;
                sb.append(now).append(" ");
            } else {
                break;
            }
        }
        System.out.println(sb);
    }

==============================================================================

中级算法

04、二维数组的查找

给定一个二维数组和target目标值、如果二维数组中有target则返回true
该二维数组:同一行的数据向右 / 同一列的数据向下 是递增的!
在这里插入图片描述
将矩阵逆时针旋转45°、则可以看成一个二叉排序树:左小右大。右上角/左下角元素等价于根结点

    private static boolean findTargetInMatrix(int[][] matrix, int target){
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
            return false;
        }
        int row = matrix.length, columns = matrix[0].length;
        int i = row,j = 0;
        //从【左下角元素开始排查】、比该元素小的都在其上方、比他大的都在右边。
        // (从右上角元素排查也可以、因为他们)
        while(i >= 0 && j < columns)
        {
            if(matrix[i][j] > target){
                i--;
            } else if(matrix[i][j] < target){
                j++;
            } else{
                return true;
            }
        }
        return false;
    }

07、重构二叉树【分治算法】

给你一个二叉树的【先序遍历、中序遍历】结果。请你重构出二叉树
先序遍历的特点:第一个值就是二叉树的根结点
按照:根节点、左子树根节点、左子树根节点的左子树根节点 、… … 、再右子树根节点…的顺序

中序遍历的特点:给定一个根节点、在中序遍历的结果中。该节点的左边一系列节点构成左子树、
右边一系列节点构成其右子树

因此:我们可以根据先序遍历第一个值(根节点)在中序遍历结果中的索引、分出其左右子树、然后递归下去
在这里插入图片描述

    private static int[] preorder;
    private static HashMap<Integer, Integer> dic = new HashMap<>();
    private static TreeNode rebuildBinaryTree(int[] preOrder, int[] inOrder){
        preorder = preOrder;
        if (preOrder.length == 1 && inOrder.length == 1){
            return new TreeNode(preOrder[0]);
        }
        // 为了提升效率(快速寻找根节点在中序遍历里的位置)、
        // 用HashMap(val,index)记录中序遍历的结果
        for(int i = 0; i < inOrder.length; i++){
            dic.put(inOrder[i], i);
        }
        return recur(0,0,inOrder.length - 1);
    }
//  【root:根节点在先序遍历中的索引 left/right:左/右结点在中序遍历里的索引
    // left~right标记了当前子树的结点、在中序遍历数组中的索引范围】
    private static TreeNode recur(int root, int left, int right) {
        // 如果中序遍历中的 左索引 > 右索引:递归终止
        if(left > right){
            return null;
        }
        // new出当前根节点
        TreeNode node = new TreeNode(preorder[root]);
        // 找到当前根节点在中序遍历中的下标,根据下标一分为二:左边属于左子树、右边属于右子树
        int rootIndex = dic.get(preorder[root]);
        // 当前根节点的左指针、指向左子树的根节点
        // root+1:根据先序遍历的特点、下一个值(root+1)就是当前根节点的左节点(左子树的根节点)
        // 左子树结点在中序遍历数组里的范围为left ~ rootIndex-1
        // (rootIndex - 1:当前根节点在中序遍历中的索引再往左移一位)
        node.left = recur(root + 1, left, rootIndex - 1);
        // 当前根节点的右指针、指向右子树的根节点
        // 右子树的根节点在先序遍历数组里的索引:root + rootIndex - left + 1
        //                              即:根节点索引 + 左子树长度 + 1)
        // 右子树结点的范围为rootIndex + 1 ~ right
        // (rootIndex + 1:当前根节点在中序遍历中的索引再往右移一位)
        node.right = recur(root + rootIndex - left + 1, rootIndex + 1, right);
        // 返回当前子树的根节点。
        return node;
    }

    public static void main(String[] args) {
        int[] preOrder = {3,9,20,15,7};
        int[] inOrder = {9,3,15,20,7};
        TreeNode root = rebuildBinaryTree(preOrder, inOrder);
        bfs(root);
        dfs(root);
    }

    // 广度优先遍历
    public static void bfs(TreeNode root){
        LinkedList<TreeNode> list = new LinkedList<>();
        list.add(root);
        while (list != null && list.size() > 0){
            TreeNode node = list.removeFirst();
            System.out.println(node.val);
            if (node.left != null){
                list.add(node.left);
            }
            if (node.right != null){
                list.add(node.right);
            }
        }
    }
    // 先序遍历
    public static void dfs(TreeNode root){
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (stack != null && stack.size() > 0){
            TreeNode node = stack.pop();
            System.out.println(node.val);
            if (node.right != null){
                stack.push(node.right);
            }
            if (node.left != null){
                stack.push(node.left);
            }
        }
    }

    static class TreeNode{
        private int val;
        private TreeNode left;
        private TreeNode right;
        public TreeNode(int value){
            this.val = value;
        }
    }

12、矩阵中的路径【回溯+递归】

回溯+递归
这里涉及到4个方向的查找、巧妙之处在于:用 短路或 || 、来联合4个方向的查找

// 格子是否已经被走过
    private static boolean[][] flag = new boolean[4][4];
    public static void main(String[] args) {
        char[][] chars = new char[4][4];
        chars[0][0] = 'A';
        chars[0][1] = 'B';
        chars[0][2] = 'C';
        chars[0][3] = 'E';
        chars[1][0] = 'S';
        chars[1][1] = 'F';
        chars[1][2] = 'C';
        chars[1][3] = 'S';
        chars[2][0] = 'A';
        chars[2][1] = 'D';
        chars[2][2] = 'E';
        chars[2][3] = 'E';
        System.out.println(findWordInMatrix(chars, "ABCCED"));
    }
    private static boolean findWordInMatrix(char[][] board, String word) {
        // 先假设一个起点、然后再递归找邻格
        for (int j = 0; j < board.length; j++) {
            for (int i = 0; i < board[0].length; i++) {
                if (find(board, word, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }
    private static boolean find(char[][] board, String word, int i, int j, int index) {
        // 本轮i j不合法、或格子走过了、或字符不匹配、返回false(回溯)
        if (i < 0 || i >= board[0].length || j < 0 || j >= board.length || flag[i][j] || board[i][j] != word.charAt(index)) {
            return false;
        }
        //  ——> 否则:说明 本次格子符合条件
        // 设置该格为已走过
        flag[i][j] = true;
        // (方法出口):本轮index等于word.length-1:即全部字符都找完了
        if (index == word.length() - 1) {
            return true;
        }
        // 巧妙的应用短路或||  来进行四个方向的查找
        // 短路或的特性:四个方向只要有一个方向返回true、说明这条路走得通
        // 如果四个方向全为 false、说明走到死胡同了(该路径不对)。则回溯到上一格。
        boolean res = find(board, word, i + 1, j, index + 1) || find(board, word, i - 1, j, index + 1) ||
                      find(board, word, i, j + 1, index + 1) || find(board, word, i, j - 1, index + 1);
        // 如果回溯回来了:就将本轮走的格子重置为 未走过
        flag[i][j] = false;
        return res;
    }

13、机器人的运动路径

我的解法:时间复杂度最多也就是O(mn)、

private static int robotPath(int m, int n, int k){
        int result = 0;
        outer : for (int i = 0; i < m; i++) {
            if ((i % 10 + i / 10) > k){
                break;
            }
            for (int j = 0; j < n; j++) {
                int now = i % 10 + i / 10 + j % 10 + j / 10;
                if (now > k){
                    continue outer;
                }
                result++;
            }
        }
        return result;
    }

力扣解法(类似于12题 回溯+递归):

	private static int m,n,k;
    private static boolean[][] visited;
    private static int robotPath(int x, int y, int z){
        m = x;
        n = y;
        k = z;
        visited = new boolean[x][y];
        return dfs(0,0) + 1;
    }

    private static int dfs(int i,int j){
    // 如果这次不满足条件(出口):回溯:返回0(这个方格访问不到)
        if (i >= m || j >= n || (i % 10 + i / 10 + j % 10 + j / 10) >= k || visited[i][j]){
            return 0;
        }
		// 否则:访问的到:设为已访问过
        visited[i][j] = true;
        // 递归:查看向右或向下的邻格是否符合条件
        return 1 + dfs(i+1, j) + 1 + dfs(i, j+1);
    }
}

14、切绳子 I (数学推导)

将一根长为n的绳子、切成长度为m的小段、如何切?使得切分出的各段的长度乘积最大?
eg:n = 8; m = 3; 切出(8 = 3+3+2):3 * 3 * 2 = 18

    private static int cut(int n){
        // 根据【数学推导】:切出的绳子长度为3或最接近3时、乘积最大
        if (n <= 3){
            return n-1;
        }
        // a:分为几个段 b: 分完后余下几
        int a = n / 3, b = n % 3;
        if (b == 0){
            return (int) Math.pow(3, a);
        }
        if (b == 1){
            // 将一个 1+3 转为 2 + 2
            return (int) Math.pow(3, a - 1) * 2 * 2;
        }
        // 直接再乘以余出来的2
        return (int) Math.pow(3, a) * 2;
    }

31、栈的压入、弹出顺序

以指定的顺序压入栈后,判定该弹出顺序是否合法。

    public boolean validStackSequences(int[] pushed, int[] popped) {
        // 使用一个栈来模拟这个压入弹出顺序,如果最后栈为空,说明弹出顺序合法(能够顺利弹出)
        Stack<Integer> stack = new Stack<>();
        int i = 0;
        for (int item : pushed) {
            stack.push(item);
            while (!stack.isEmpty() && stack.peek().equals(popped[i])) {
                stack.pop();
                i++;
            }
        }
        return stack.isEmpty();
    }

34、二叉树中和为某值的路径

    private List<LinkedList<Integer>> findSumTarget(TreeNode head, int target) {
        this.target = target;
        find(head);
        return result;
    }

    private void find(TreeNode head) {
        if (head == null || list.stream().reduce(Integer::sum).orElse(0) + head.value > target) {
            return;
        }
        list.add(head.value);
        if (list.stream().reduce(Integer::sum).orElse(0) == target && head.left == null && head.right == null) {
            result.add(new LinkedList(list));
        }
        find(head.left);
        find(head.right);
        list.removeLast();
    }

36、二叉搜索树改造为循环链表

    // 定义双向链表的头结点head。  每次递归的前驱节点pre
    static TreeNode pre, head;

    public static TreeNode treeToDoublyList(TreeNode root) {
        if (root == null) return null;
        dfs(root);
        // 双向链表形成后。将头尾节点的指针互相连起来,形成循环
        head.left = pre;
        pre.right = head;
        return head;
    }

    static void dfs(TreeNode cur) {
        // 二叉搜索树中序遍历的结果就是递增的。因此根据中序递归遍历来改造
        if (cur == null) {
            return;
        }
        // 递归左子树
        dfs(cur.left);
        //-----------业务逻辑处理(原生中序遍历是输出节点值sout(root.value) / 但这里是需要修改指针朝向)---------
        if (pre != null) {
            // pre!=null说明本次递归非头结点,则需要修改双向指针pre.right  cur.left
            pre.right = cur;
        } else {
            // pre==null,代表正在访问链表头的结点。将该节点记为head。将head.left指向pre即可
            head = cur;
        }
        cur.left = pre;
        pre = cur;
        //--------------------
        // 递归左子树
        dfs(cur.right);
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Binary H.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值