剑指 offer 刷题集

目录

数组

1. LCR 121. 寻找目标值 - 二维数组

2. LCR 120. 寻找文件副本

3. LCR 128. 库存管理 I

4. LCR 131. 砍竹子 I

5. LCR 132. 砍竹子 II

6. LCR 135. 报数

7. LCR 139. 训练计划 I

8. LCR 158. 库存管理 II

9. LCR 159. 库存管理 III

10. LCR 160. 数据流中的中位数

11. LCR 164. 破解闯关密码

12. LCR 168. 丑数

13. LCR 170. 交易逆序对的总数

14. LCR 172. 统计目标成绩的出现次数

15. LCR 173. 点名

16. LCR 179. 查找总价格为目标值的两个商品

17. LCR 180. 文件组合

18. LCR 183. 望远镜中最高的海拔

19. LCR 186. 文物朝代判断

20. LCR 189. 设计机械累加器

21. LCR 191. 按规则计算统计结果

字符串

22. LCR 122. 路径加密

23. LCR 138. 有效数字

24. LCR 157. 套餐内商品的排列顺序

25. LCR 167. 招式拆解 I

26. LCR 169. 招式拆解 II

27. LCR 181. 字符串中的单词反转

28. LCR 182. 动态口令

29. LCR 192. 把字符串转换成整数 (atoi)

链表

30. LCR 123. 图书整理 I

31. LCR 136. 删除链表的节点

32. LCR 140. 训练计划 II

33. LCR 141. 训练计划 III

34. LCR 142. 训练计划 IV

35. LCR 171. 训练计划 V

36. LCR 154. 复杂链表的复制

栈和队列

37. LCR 147. 最小栈

38. LCR 125. 图书整理 II

39. LCR 184. 设计自助结算系统

40. LCR 148. 验证图书取出顺序

41. LCR 124. 推理二叉树

42. LCR 143. 子结构判断

43. LCR 144. 翻转二叉树

44. LCR 145. 判断对称二叉树

45. LCR 149. 彩灯装饰记录 I

46. LCR 150. 彩灯装饰记录 II

47. LCR 151. 彩灯装饰记录 III

48. LCR 152. 验证二叉搜索树的后序遍历序列

49. LCR 153. 二叉树中和为目标值的路径

50. LCR 155. 将二叉搜索树转化为排序的双向链表

51. LCR 156. 序列化与反序列化二叉树

52. LCR 174. 寻找二叉搜索树中的目标节点

53. LCR 175. 计算二叉树的深度

54. LCR 176. 判断是否为平衡二叉树

55. LCR 193. 二叉搜索树的最近公共祖先

56. LCR 194. 二叉树的最近公共祖先

位运算

57. LCR 133. 位 1 的个数

58. LCR 134. Pow(x, n)

59. LCR 177. 撞色搭配

60. LCR 178. 训练计划 VI

61. LCR 190. 加密运算

动态规划

62. LCR 126. 斐波那契数

63. LCR 127. 跳跃训练

64. LCR 137. 模糊搜索验证

65. LCR 161. 连续天数的最高销售额

66. LCR 165. 解密数字

67. LCR 166. 珠宝的最高价值

68. LCR 185. 统计结果概率

69. LCR 187. 破冰游戏

70. LCR 129. 字母迷宫

71. LCR 130. 衣橱整理​编辑

72. LCR 146. 螺旋遍历二维数组

贪心算法

73. LCR 188. 买卖芯片的最佳时机

找规律

74. LCR 162. 数字 1 的个数

75. LCR 163. 找到第 k 位数字


数组

1. LCR 121. 寻找目标值 - 二维数组

就是根据矩阵特性,来查找矩阵值,每行从左到右是递增的,每一列从上到下是递增的。

我们可以从数组的右上角 arr[i][j] 开始,如果 target 大于arr[i][j],那就 i++ 往后走,如果比它小,那就 j-- 往前走,如果相等返回 true,数组越界了也没找到该值就返回 false。

代码实现:

class Solution {
    public boolean findTargetIn2DPlants(int[][] plants, int target) {
        // 注意数组没有值的情况
        if (plants.length == 0) return false;
        // 找到右上角的值
        int i = 0;
        int j = plants[0].length - 1;
        while (i >= 0 && i < plants.length && j >= 0 && j < plants[0].length) {
            if (plants[i][j] > target) {
                j--;
            } else if (plants[i][j] < target) {
                i++;
            } else {
                return true;
            }
        }
        return false;
    }
}

2. LCR 120. 寻找文件副本

寻找重复出现数字,直接两层遍历暴力破解即可。

或者直接调用库函数给数组排下序,然后再相邻两两比较,相同就返回。

或者用 set 来做,遍历数组,如果元素在 set 中存在,则返回元素,如果不存在,就把元素添加进 set 里。

代码实现:

class Solution {
    public int findRepeatDocument(int[] documents) {
        for (int i = 0; i < documents.length - 1; i++) {
            for (int j = i + 1; j < documents.length; j++) {
                if (documents[i] == documents[j]) {
                    return documents[i];
                }
            }
        }
        return -1;
    }
}



class Solution {
    public int findRepeatDocument(int[] documents) {
        Arrays.sort(documents);
        for (int i = 0; i < documents.length - 1; i++) {
            if (documents[i] == documents[i + 1]) {
                return documents[i];
            }
        }
        return -1;
    }
}




class Solution {
    public int findRepeatDocument(int[] documents) {
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < documents.length; i++) {
            int x = documents[i];
            if (set.contains(x)) {
                return x;
            } else {
                set.add(x);
            }
        }
        return -1;
    }
}

3. LCR 128. 库存管理 I

这个简单,直接遍历数组找最小元素即可。

也可以用二分查找,因为是部分区间是升序的,就可以利用二分查找来完成。

以 s[right] 为参照物, 用中间值 s[mid] 与 s[right] 比较,如果 s[mid] > s[right] ,如例 1 ,则说明最小元素在 mid + 1, right 区间,如果小于,则说明最小元素在 left, mid 区间,如果相等,就无法以判断 right 来判断最小元素在哪里,所以 right-- 往前找另一个参照物。最后 left 与 right 重合时就是最小值的下标。

代码实现:

class Solution {
    public int stockManagement(int[] stock) {
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < stock.length; i++) {
            if (stock[i] < min) {
                min = stock[i];
            }
        }
        return min;
    }
}


// 二分查找
class Solution {
    public int stockManagement(int[] stock) {
        int left = 0;
        int right = stock.length - 1;
        while (left < right) {
            int mid = (left + right) / 2;
            if (stock[mid] > stock[right]) {
                left = mid + 1;
            } else if (stock[mid] == stock[right]) {
                right--;
            } else {
                right = mid;
            }
        }
        return stock[right];
    }
}

4. LCR 131. 砍竹子 I

可以用动态规划来做,dp[i] 就是长度为 i 的竹子拆分两段或以上的最大乘积

i 拆分分两种情况,第一,只拆分成 2 段,j 和 i - j。 乘积就是 j * (i - j)

第二,拆分成两段以上,j , i - j 还继续拆分,乘积就是 j * dp[i - j]

所以 dp[i] 就是上面两种情况的结果集合中最大值。

初始化:因为 i = 0 和 i = 1 时不能拆分,所以 dp[0] = dp[1] = 0。

最后返回 dp[bl] 即可。

代码实现:

class Solution {
    public int cuttingBamboo(int bamboo_len) {
        // dp[i] 是长度为 i 的竹子拆分为两段或以上的最大乘积
        int[] dp = new int[bamboo_len + 1];
        // 初始化 dp[0] = dp[1] = 0
        dp[0] = 0;
        dp[1] = 0;
        for (int i = 2; i <= bamboo_len; i++) {
            // 分两种情况,一种是竹子 i 只拆分成两个数字 3 = 2 + 1
            // 分别是 j 和 i - j,另一种是 j,和 i - j 继续拆分成更细的 3 = 1 + 1 + 1
            // 所以需要 j 从 i - 1 开始遍历,j 》= 1,然后求以上结果集合中的最大的那一个
            for (int j = i - 1; j >= 1; j--) {
                int max = Math.max(j * (i - j), j * dp[i - j]);
                dp[i] = Math.max(dp[i], max);
            }
        }
        return dp[bamboo_len];
    }
}

5. LCR 132. 砍竹子 II

因为范围变大了,所以就不能用动态规划了。

看题解大佬的思路是将绳子长度尽可能分为多个 3,这样乘积才最大。

所以当 n < 4 的时候,返回 n - 1,当 n 等于 4 的时候,返回 4,当 n > 4 的时候,就要切割成 3 的小端,只要 n > 4,就进行切割,然后将切割出来的 3 累乘,然后取模。

代码实现:

class Solution {
    public int cuttingBamboo(int n) {
        if (n < 4) {
            return n - 1;
        } else if (n == 4) {
            return n;
        } else {
            long ans = 1;
            while (n > 4) {
                ans *= 3;
                ans %= 1000000007;
                n -= 3;
            }
            return (int) (ans * n % 1000000007);
        }
    }
}

6. LCR 135. 报数

就是算出一共有几个数字,然后数组赋值即可。

第二种方法就是全排列,参考大佬的解答,假如数字特别大,就无法用整型来存储数据了,那此时就可以用字符串来存储数据,要生成从 1 到 10^n - 1 的数,我们可以看作是数字 0~9 的排列问题,要循环生成 n 位数,首先是生成第一位数字,第一位一定是 1 ~ 9,然后再递归生成剩下的数字,从 0 ~ 9 中取值,

代码实现:

class Solution {
    public int[] countNumbers(int cnt) {
        int len = (int) Math.pow(10, cnt);
        int[] arr = new int[len - 1];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i + 1;
        }
        return arr;
    }
}



// 全排列
class Solution {
    int count = 0;
    int ret[];

    public int[] countNumbers(int cnt) {
        int len = (int) Math.pow(10, cnt) - 1;
        ret = new int[len];

        // i 表示要生成 i 位数
        for (int i = 1; i <= cnt; i++) {
            // 首先生成第一位
            for (char first = '1'; first <= '9'; first++) {
                // 创建个字符数组存储生成的数字
                char[] num = new char[i];
                num[0] = first;
                // 然后递归生成剩下的数字
                dfs(1, num, i);
            }
        }
        return ret;
    }

    // digit 表示要生成 digit 位数
    public void dfs(int index, char[] num, int digit) {
        if (index == digit) {
            // 接下来要生成 index 下标的数字,
            // 此时说明已经生成了 digit 位数,将其存入数组中并返回即可
            ret[count++] = Integer.valueOf(String.valueOf(num));
            return;
        }
        // 生成范围是 0 ~ 9
        for (char i = '0'; i <= '9'; i++) {
            num[index] = i;
            // 继续生成剩下的数字
            dfs(index + 1, num, digit);
        }
    }
}

7. LCR 139. 训练计划 I

这个简单,用双指针就行,left 从 0 下标开始找偶数,right 从最后一个位置开始找奇数,然后交换 left 和 right 下标的值,然后 left 和 right 再继续往后找下一组,直到 left 和 right 相遇。

代码实现:

class Solution {
    public int[] trainingPlan(int[] actions) {
        // 这个简单
        int left = 0;
        int right = actions.length - 1;
        while (left < right) {
            // left 往后走找偶数
            while (left < right && actions[left] % 2 == 1) {
                left++;
            }

            // right 往前找奇数
            while (left < right && actions[right] % 2 == 0) {
                right--;
            }
            // 此时 left 一定是偶数, right 一定是奇数,交换
            int tmp = actions[left];
            actions[left] = actions[right];
            actions[right] = tmp;
        }
        return actions;
    }
}

8. LCR 158. 库存管理 II

这个也简单,将数组排序,排完序后,下标为 arr.length / 2 的元素就一定是出现次数超过一半的数字。还有另一种方法,叫做摩尔投票法,简单来说就是假设该数是众数 x ,那么遇到 x 就 count++,不是 x 就 count--,如果此时 count 为 0 了,那么说明 x 一定不是众数,因为众数的出现次数大于数组的一半,所以遍历完数组,最后 count 应该是大于 0 的。 

代码实现:

class Solution {
    public int inventoryManagement(int[] stock) {
        Arrays.sort(stock);
        return stock[stock.length / 2];
    }
}


// 投票法
class Solution {
    public int inventoryManagement(int[] stock) {
        int count = 1;
        int x = stock[0];
        for (int i = 1; i < stock.length; i++) {
            if (count == 0) {
                // 当票数为 0 时,则说明此时 x 不是众数
                // 就需要重新假设当前数字为众数
                x = stock[i];
            }
            if (x == stock[i]) {
                // 数字相同,则票数++
                count++;
            } else {
                // 数字不同,则票数--
                count--;
            }
        }
        return x;
    }
}

9. LCR 159. 库存管理 III

这个简单,就是先排序,然后返回范围从 0 到 cnt - 1 下标的数组即可。

也可以用优先级队列来做,求前 k 大的元素,建立小根堆,然后遍历数组,前 k 个入队,从 第 k+1 个元素开始与堆顶元素比较,如果 k+1 元素大于堆顶元素,那么堆顶元素出队,第 k+1 元素入队,遍历完数组后,堆顶元素就是第 k 大的元素。求前 k 小元素,就建立大根堆,与上面思路类似。

代码实现:

class Solution {
    public int[] inventoryManagement(int[] stock, int cnt) {
        Arrays.sort(stock);
        return Arrays.copyOfRange(stock, 0, cnt);
    }
}


// 利用优先级队列解决 top-k 问题
class Solution {
    public int[] inventoryManagement(int[] stock, int cnt) {
        if (stock == null || stock.length == 0 || cnt <= 0) return new int[0];
        // 或者利用大根堆来做,就是 tok-k 问题
        PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        for (int i = 0; i < stock.length; i++) {
            if (i < cnt) {
                queue.offer(stock[i]);
            } else {
                int top = queue.peek();
                if (top > stock[i]) {
                    queue.poll();
                    queue.offer(stock[i]);
                }
            }
        }
        // 最后队列里就是前 cnt 个小的元素
        int[] ret = new int[cnt];
        for (int i = cnt - 1; i >= 0; i--) {
            ret[i] = queue.poll();
        }
        return ret;
    }
}

10. LCR 160. 数据流中的中位数

如图,题目让你找有序数组中的中位数,那就是先定义一个数组 arr 用来存放数据,然后再定义 usedSize 记录数组大小,然后写新增元素方法,那就是先判断数组满没满,满了就扩容,然后再新增元素,写寻找中位数时,可以先排序这个数组,使用直接插入排序即可,然后再来找到中位数。

看了大佬的题解,发现可以用两个优先级队列来做,队列 A 存储较大的一半数字(小根堆),队列 B 存储较小的一半数字(大根堆)。

新增元素 num 时,当 A 的元素个数等于 B 的元素个数时,需要往 A 队列增加元素,先把 num 添加进 B 队列中,然后再让 B 队列的堆顶元素添加进 A 队列中。当 A 的元素个数不等于 B 的元素个数时,需要往 B 添加元素,将 num 放入 A 队列中,然后再将 A 队列的堆顶元素放入 B 队列中。

查找中位数,如果两个队列的元素个数不相等,那就返回 A 队列的堆顶元素,如果元素个数相等,那就返回两队列的堆顶元素的和除以 2.0。

代码实现:

class MedianFinder {

    /**
     * initialize your data structure here.
     */
    int usedSize = 0;
    int[] arr;

    public MedianFinder() {
        arr = new int[10];

    }

    public void addNum(int num) {
        // 扩容
        if (usedSize == arr.length) {
            this.arr = Arrays.copyOf(arr, arr.length * 2);
        }
        arr[usedSize] = num;
        usedSize++;
    }

    public double findMedian() {
        if (usedSize == 0) {
            return -1.0;
        }

        // 直接插入排序
        for (int i = 1; i < usedSize; i++) {
            int j = i - 1;
            int tmp = arr[i];
            for (; j >= 0; j--) {
                if (arr[j] > tmp) {
                    arr[j + 1] = arr[j];
                } else {
                    break;
                }
            }
            arr[j + 1] = tmp;
        }
        if (usedSize % 2 == 1) {
            return (double) arr[usedSize / 2];
        } else {
            double sum = arr[usedSize / 2] + arr[usedSize / 2 - 1];
            return sum / 2;
        }

    }
}



// 利用两个优先级队列来解决
class MedianFinder {
    PriorityQueue<Integer> A, B;

    /**
     * initialize your data structure here.
     */
    public MedianFinder() {
        // A 是小根堆,B 是大根堆
        A = new PriorityQueue<Integer>();
        B = new PriorityQueue<Integer>(new Comparator<Integer>() {
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
    }

    public void addNum(int num) {
        if (A.size() == B.size()) {
            // 则需要往 A 队列中添加元素
            B.offer(num);
            A.offer(B.poll());
        } else {
            // 需要往 B 队列添加元素
            A.offer(num);
            B.offer(A.poll());
        }

    }

    public double findMedian() {
        if (A.size() == B.size()) {
            return (A.peek() + B.peek()) / 2.0;
        } else {
            return A.peek();
        }

    }
}

11. LCR 164. 破解闯关密码

这道题本质上就是排序问题,字符串之间的排序,可以用冒泡排序来写。

比如示例 2: 

0,3,30,34,5,9

0 和 3 不需要交换,因为 03 <= 30 ,         0,3,30,34,5,9

3 和 30 需要交换,因为 330 > 303,          0,30,3,34,5,9

3 和 34 不需要交换,因为 334 <= 343,    0,30,3,34,5,9

34 和 5 不需要交换,因为 345 <= 543,    0,30,3,34,5,9

5 和 9 不需要交换,因为 59 <= 95,          0,30,3,34,5,9

那假设数组后面还有个 1 的话:0,3,30,34,5,9,1

那么 9 和 1 需要交换,0,30,3,34,5,1,9,而 1 很明显应该要在 0 之后,所以排序需要多来几趟,就可以用冒泡排序,根据相邻两个数字的拼接顺序的后的大小,来判断两个数字要不要交换。排完序后就再次遍历数组,拼接数组中元素,然后返回字符串即可。

代码实现:

class Solution {
    public String crackPassword(int[] password) {
        // 冒泡排序
        for (int i = 0; i < password.length - 1; i++) {
            boolean flg = true;
            for (int j = 0; j < password.length - 1 - i; j++) {
                // 8 和 7 --> 87 和 78  , 87 > 78, 则需要交换 
                String s1 = password[j] + "" + password[j + 1];
                String s2 = password[j + 1] + "" + password[j];
                if (s1.compareTo(s2) > 0) {
                    int tmp = password[j];
                    password[j] = password[j + 1];
                    password[j + 1] = tmp;
                    flg = false;
                }
            }
            if (flg) break;
        }
        // 最后遍历数组,拼接字符串即可
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < password.length; i++) {
            s.append(password[i]);
        }
        return s.toString();
    }
}




// 解法二:重写排序方法
class Solution {
    public String crackPassword(int[] password) {
        List<String> arr = new ArrayList<>();
        for (int num : password) {
            arr.add(String.valueOf(num));
        }
        arr.sort(new Comparator<String>() {
            public int compare(String s1, String s2) {
                return (s1 + s2).compareTo(s2 + s1);
            }
        });
        StringBuilder ans = new StringBuilder();
        for (String x : arr) {
            ans.append(x);
        }
        return ans.toString();
    }
}

12. LCR 168. 丑数

根据题目可以发现,第 n 个丑数其实就是通过前面的丑数 * 2,或者丑数 * 3,或者丑数 * 5,并且按照升序排列得到的,所以这道题可以用动态规划来做,dp[i] 就表示第 i 个丑数的值。

我们可以先定义三个指针,p2,p3,p5。

p2 表示 dp 数组中还没使用过乘 2 机会的,数字的下标(p2 前面的下标对应的数字都使用过 * 2 得到了一个丑数)。p3 和 p5 同理。所以在一开始,p2 和 p3 和 p5 都指向初始位置 1,dp[1] = 1。

然后 dp[i] 就等于 还没使用过乘 2 机会的数字 * 2,还没使用过乘 3 机会的数字 * 3,还没使用过乘 5 机会的数字 * 5,中的最小值,然后判断使用这三个指针是否得到了 dp[i],如果是,那么对应的下标往后走。最后返回 dp[n] 即可。

代码实现:

class Solution {
    public int nthUglyNumber(int n) {
        int[] dp = new int[n + 1];
        dp[1] = 1;
        // p2 表示 dp 数组中还没使用乘 2 机会的第一个数字所对应的下标
        int p2 = 1, p3 = 1, p5 = 1;
        for (int i = 2; i <= n; i++) {
            int num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5;
            // dp[i] 就是这三个数字中最小的那个
            dp[i] = Math.min(num2, num3);
            dp[i] = Math.min(dp[i], num5);
            if (dp[i] == num2) {
                // 当前下标的数字已经使用过乘 2 机会了,那么 p2 需要往后走
                p2++;
            }
            if (dp[i] == num3) {
                p3++;
            }
            if (dp[i] == num5) {
                p5++;
            }
        }
        return dp[n];
    }
}

13. LCR 170. 交易逆序对的总数

第一眼,看上就就是两次遍历,前一个大于后一个,那就 count++,最后返回 count,但是超时了。看题解发现能用归并排序来做,在合并数组的时候,如果 arr[i] (i >= left && i <= mid)大于 arr[j] (j > mid && j <= right) ,则说明至少有 mid - i + 1 个逆序对,用 count 来记录,最后返回 count 即可。

代码实现:

class Solution {
    int count = 0;

    public int reversePairs(int[] record) {
        // 可以用归并排序来做
        mergeSort(record, 0, record.length - 1);
        return count;
    }

    public void mergeSort(int[] arr, int left, int right) {
        // 只有一个元素,不用排序
        if (left >= right) return;

        // 二分
        int mid = (left + right) / 2;
        // 分割左边
        mergeSort(arr, left, mid);
        // 分割右边
        mergeSort(arr, mid + 1, right);
        // 合并
        merge(arr, left, mid, right);
    }

    public void merge(int[] arr, int left, int mid, int right) {
        int[] tmpArr = new int[right - left + 1];
        // 合并有序数组
        int i = left, j = mid + 1, k = 0;
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                tmpArr[k++] = arr[i++];
            } else {
                // 因为 arr[i] > arr[j]
                // 所以 left 到 mid 区间的所有元素都比 arr[j] 大
                // [1,3,5,9]  [2,6,7]
                // 3 > 2, 所以 3,5,9 也大于 2
                count += (mid - i + 1);
                tmpArr[k++] = arr[j++];
            }
        }
        while (i <= mid) {
            tmpArr[k++] = arr[i++];
        }
        while (j <= right) {
            tmpArr[k++] = arr[j++];
        }
        // 拷贝回原数组
        for (i = left; i <= right; i++) {
            arr[i] = tmpArr[i - left];
        }
    }
}

14. LCR 172. 统计目标成绩的出现次数

这个很简单,直接遍历数组,看看当前元素与 target 是否相同,相同就 count++,最后返回 count 即可。还可以用二分查找来做,先查找出数组最前面的 target 的下标,然后再找出最后面 target 的下标,然后返回下标差 + 1 即可。

代码实现:

class Solution {
    public int countTarget(int[] scores, int target) {
        int count = 0;
        for (int i = 0; i < scores.length; i++) {
            if (scores[i] == target) {
                count++;
            }
        }
        return count;
    }
}


// 二分查找
class Solution {
    public int countTarget(int[] scores, int target) {
        if (scores == null || scores.length == 0) return 0;
        // 通过二分查找找出 target 的左边界
        int left = 0, right = scores.length - 1;
        while (left < right) {
            int mid = (left + right) / 2;
            if (scores[mid] >= target) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        // 如果没找到,说明数组没有 target
        if (scores[left] != target) return 0;
        int l = left;

        // 通过二分查找找出 target 的右边界
        left = 0;
        right = scores.length - 1;
        while (left < right) {
            int mid = (left + right + 1) / 2;
            if (scores[mid] <= target) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        return right - l + 1;
    }
}

15. LCR 173. 点名

这个简单,可以把 n 位同学的学号加起来得到 sum (从 1 加到 n),然后再遍历数组,用 sum 减去数组的和就能得到缺席的同学学号。

也可以用哈希表的方法来写,记录数组中每个元素的出现次数,然后遍历哈希表,第一次遇到出现次数为 0 的就是缺失的数字。

也可以看题能得知,数组下标与数组学号是相同的,如果不相同,那么数组下标就是缺失的数字。

也可以用二分查找,缺失的元素在数组的右区间,如果 mid == arr[mid] ,则说明左边不存在缺失元素,所以会来到 [mid + 1, right] 区间来寻找缺失元素。如果不相等,则说明缺失元素很可能就是 mid 本身,所以 right = mid

代码实现:

class Solution {
    public int takeAttendance(int[] records) {
        int sum = 0;
        for (int i = 0; i <= records.length; i++) {
            sum += i;
        }
        for (int i = 0; i < records.length; i++) {
            sum -= records[i];
        }
        return sum;
    }
}



// 哈希表
class Solution {
    public int takeAttendance(int[] records) {
        int[] hash = new int[records.length + 1];
        for (int i = 0; i < records.length; i++) {
            hash[records[i]]++;
        }
        for (int i = 0; i < hash.length; i++) {
            if (hash[i] == 0) return i;
        }
        return 0;
    }
}



class Solution {
    public int takeAttendance(int[] records) {
        for (int i = 0; i < records.length; i++) {
            if (i != records[i]) return i;
        }
        return records[records.length - 1] + 1;
    }
}


// 二分查找
class Solution {
    public int takeAttendance(int[] records) {
        int left = 0, right = records.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (mid == records[mid]) {
                // 去右边找,不见的元素在数组右边
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        // 0 1 2 3 4
        if (records[left] == left) return left + 1;
        return left;
    }
}

16. LCR 179. 查找总价格为目标值的两个商品

第一眼就是直接用两次遍历完成,但是发现超时了,可以用双指针来写,定义 left 指向数组最左边,right 指向数组最右边,数组递增,如果 arr[left] + arr[right] > target,说明从arr[left] 与 right 往后的值相加得到的结果还是大于 target,所以此时 right 应该往前走。如果 arr[left] + arr[right] < target,说明 arr[left] 太小了,所以应该 left++ 往后走,当 arr[left] + arr[right] == target 时,直接返回即可。

代码实现:

class Solution {
    public int[] twoSum(int[] price, int target) {
        int left = 0, right = price.length - 1;
        while (left < right) {
            if (price[left] + price[right] > target) {
                right--;
            } else if (price[left] + price[right] < target) {
                left++;
            } else {
                return new int[]{price[left], price[right]};
            }
        }
        return new int[]{0, 0};
    }
}

17. LCR 180. 文件组合

直接暴力枚举,用两层 for 循环搞定。 

还可以用滑动窗口来做,大致思路就是进窗口,判断,出窗口。

代码实现:

class Solution {
    public int[][] fileCombination(int target) {
        ArrayList<int[]> ret = new ArrayList<>();
        for (int i = 1; i < target; i++) {
            int sum = i;

            for (int j = i + 1; j < target; j++) {
                if (sum == target) {
                    int[] arr = new int[j - i];
                    for (int k = i; k < j; k++) {
                        arr[k - i] = k;
                    }
                    ret.add(arr);
                    break;
                } else if (sum > target) {
                    break;
                } else {
                    sum += j;
                }
            }
        }
        return ret.toArray(new int[ret.size()][]);
    }
}




class Solution {
    public int[][] fileCombination(int target) {
        ArrayList<int[]> ret = new ArrayList<>();
        int left = 1, right = 1;
        int sum = 0;
        while (left <= target / 2) {
            // 说明此时值太小,需要 right 往后挪动
            while (right < target && sum < target) {
                sum += right;
                right++;
            }
            if (sum == target) {
                // 记录结果
                int[] arr = new int[right - left];
                for (int i = left; i < right; i++) {
                    arr[i - left] = i;
                }
                // 此时说明 right 再往后走,sum 也一定大于 target,
                // 则需要寻找下一组 sum,那么 left 就需要往后走 
                ret.add(arr);
                sum -= left;
                left++;
            } else if (sum > target) {
                // 此时值太大,需要减去一些值,就可以减去 left
                sum -= left;
                left++;
            }
        }
        return ret.toArray(new int[ret.size()][]);
    }
}

18. LCR 183. 望远镜中最高的海拔

如图,题目明示你用滑动窗口,那这道题就可以用滑动窗口来做。

代码实现:

class Solution {
    public int[] maxAltitude(int[] heights, int limit) {
        // 如果数组没有元素,那么返回的数组也不能有元素
        if (heights.length == 0) return new int[0];
        // left 为窗口起点,right 为窗口终点
        int left = 0, right = 0;
        // count 为当前窗口大小
        int count = 0;
        // 记录返回值,k 为 ret 当前存放元素个数
        int[] ret = new int[heights.length - limit + 1];
        int k = 0;
        while (left <= heights.length - limit) {
            if (count < limit) {
                // 没到窗口大小,那就入元素
                right++;
                count++;
            } else if (count == limit) {
                // 从窗口起点到终点找最大值
                int max = Integer.MIN_VALUE;
                for (int i = left; i < right; i++) {
                    max = Math.max(max, heights[i]);
                }
                ret[k++] = max;
                // 出窗口
                left++;
                count--;
            } else {
                // 超过窗口容量,出窗口
                left++;
                count--;
            }
        }
        return ret;
    }
}

19. LCR 186. 文物朝代判断

问你数组的数字是否连续,0 可以当作万能牌,问你 5 张牌能不能组成顺子。

看到这我第一想法就是排序,然后遍历数组,看看数组是否是连续的,如果不是,那就记录差了多少个数字,然后如果万能牌 0 的个数大于等于差的数字个数,则说明数组是连续的,但是还得注意,如果有相同牌,并且相同牌不是 0 的话,则说明数组不是连续的。

看大佬的题解,发现能直接通过找数组最大值和最小值(除 0 外),然后 max - min < 5 则说明连续,那这样就好办了,可以用 set 来判重,遍历数组找最大最小值,然后判断即可。

代码实现:

class Solution {
    public boolean checkDynasty(int[] places) {
        // 先排序,然后遍历数组
        Arrays.sort(places);
        int count = 0;
        int sum = 0;
        for (int i = 0; i < places.length - 1; i++) {
            if (places[i] == 0) {
                count++;
            } else {
                // 不能有相同牌,有相同牌就不是顺子了
                if (places[i] == places[i + 1]) return false;
                // 0,0,6,7,9
                if (places[i] + 1 != places[i + 1]) {
                    sum += places[i + 1] - 1 - places[i];
                }
            }
        }
        if (count >= sum) return true;
        else return false;
    }
}



// set 去重
class Solution {
    public boolean checkDynasty(int[] places) {
        Set<Integer> set = new HashSet<>();
        int min = 14;
        int max = 0;
        for (int x : places) {
            if (x == 0) continue;
            // 重复牌
            if (set.contains(x)) {
                return false;
            }
            max = Math.max(x, max);
            min = Math.min(x, min);
            set.add(x);
        }
        return max - min < 5;
    }
}

20. LCR 189. 设计机械累加器

我们可以利用短路逻辑,用递归来写,递归出口就是当 target = 1 时,那么我们可以利用短路逻辑与,当 target > 1 && (写要执行的代码),最后再将后面部分改成值恒为真的布尔表达式,最后再返回即可。

代码实现:

class Solution {
    public int mechanicalAccumulator(int target) {
        // 用递归,可以利用短路逻辑来写
        // 如果 target <= 1,则递归停止,否则返回就相加
        boolean flg = target > 1 && (target += mechanicalAccumulator(target - 1)) > 0;
        return target;
    }
}

21. LCR 191. 按规则计算统计结果

这道题不算难,让你求 arrB[i] 为数组 arrA 中除了 arrA[i] 外所有元素的乘积,只需要把 arrA 数组遍历一遍,求出除了 0 外所有元素的乘积 mul,并记录 arrA 中 0 的个数,然后用 mul / arrA[i] 就能得到对应的 arrB[i],然后返回 arrB 即可。

但是有个毒点,那就是要考虑 arrA 中含有元素 0 的情况,但是这个 0 也要分情况讨论:

如果 0 只有一个,那么其他元素相乘就都是 0,而 0 对应的元素既是其他元素的乘积。

如果 0 有两个及以上,那么返回的数组 arrB 的所有元素就都是 0 了。

根据以上思路,代码就很好写啦。

还有另一种方法,看题解知道的可以创建个数组 left 和 right,left[i] 表示 arrA[i] 以前的元素乘积(不包括 arrA[i]),right[i] 表示 arrA[i] 以后的元素乘积(不包括 arrA[i]),这样 arrB[i] = left[i] * right[i]。

代码实现:

class Solution {
    public int[] statisticalResult(int[] arrayA) {
        int mul = 1;
        // 记录数组中 0 的个数
        int count = 0;
        int[] arrB = new int[arrayA.length];
        // 求出数组每个元素的乘积(除 0 外)
        for (int i = 0; i < arrayA.length; i++) {
            if (arrayA[i] == 0) {
                count++;
                continue;
            }
            mul *= arrayA[i];
        }
        if (count >= 2) {
            // 说明数组中有两个及以上的 0 ,则值一定全是 0
            return new int[arrayA.length];
        }

        for (int i = 0; i < arrB.length; i++) {
            if (count > 0) {
                // 此时说明数组中含有一个 0,则除 0 外所有数组都是 0
                if (count == 1) {
                    if (arrayA[i] != 0) {
                        arrB[i] = 0;
                    } else {
                        arrB[i] = mul;
                    }
                }
            } else {
                // 数组中没 0,正常执行
                arrB[i] = mul / arrayA[i];
            }
        }
        return arrB;
    }
}





class Solution {
    public int[] statisticalResult(int[] arrayA) {
        // 边界情况
        if (arrayA == null || arrayA.length == 0) return new int[0];
        int[] left = new int[arrayA.length];
        left[0] = 1;
        // 遍历数组初始化 left 的值
        for (int i = 1; i < arrayA.length; i++) {
            left[i] = left[i - 1] * arrayA[i - 1];
        }
        // 记录右边的元素乘积
        int right = 1;
        for (int i = left.length - 1; i >= 0; i--) {
            left[i] *= right;
            right *= arrayA[i];
        }
        return left;
    }
}

字符串

22. LCR 122. 路径加密

这个很简单,遍历判断即可然后用 StringBuilder 即可。

代码实现:

class Solution {
    public String pathEncryption(String path) {
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < path.length(); i++) {
            char ch = path.charAt(i);
            if (ch != '.') {
                str.append(ch);
            } else {
                str.append(' ');
            }
        }
        return str.toString();
    }
}

23. LCR 138. 有效数字

一种比较简单的做法,就是判断该字符串不是有效数字的情况,严格按照题目规定的顺序来判断字符串:按照 空格,(正负号),数字,(点),数字,(eE,数字),空格

定义四个 flg 来判断字符串中是否有数字,E/e,正负号,点。

先从 0 下标开始遍历字符串,一直跳过空格。

从非空格的地方开始判断,如果是数字,那就将数字 flg 置为 true,然后跳过这一连续的数字,然后判断下标是否已经遍历完字符串,如果是,就返回 true。

如果当前字符是 E/e,如果已经出现过 E/e 之前没有出现过数字,则返回 false,否则将 E/e flg 置为 true,再将其他三个 flg 置为 false,往 E 之后重新开始找数字。

如果当前字符是正负号,如果已经出现过符号,数字,点,就返回 false,否则将 符号 flg 置为 true。

如果当前字符是空格,则说明下标已经遍历到字符串末尾的空格,或者空格被字符之间夹起来了,则退出循环,单独判断。

如果是上述以外的字符,就返回 false。

出了循环,就处理末尾空格的情况,一直跳过空格,如果 下标为字符串长度并且数字 flg 是 true,则返回 true,否则返回 false

代码实现:

class Solution {
    public boolean validNumber(String s) {
        // 是否有数字,E,正负号,点
        boolean hasNum = false;
        boolean hasE = false;
        boolean hasSign = false;
        boolean hasDot = false;
        // 先跳过开头的空格
        int index = 0, len = s.length();
        while (index < len && s.charAt(index) == ' ') {
            index++;
        }
        // 从非空格的字符开始判断
        // 顺序严格按照 空格,正负号,数字,(点),数字,eE,数字 空格
        while (index < len) {
            if (isNum(s.charAt(index))) {
                hasNum = true;
                // 如果是数字
                while (index < len && isNum(s.charAt(index))) {
                    index++;
                }
                // 说明字符串全是数字
                if (index == len) return true;
            }
            // 到这里就字符就一定是非数字的
            char c = s.charAt(index);
            if (c == 'e' || c == 'E') {
                // 如果出现多个 E,或者之前没有出现过数字
                if (hasE || !hasNum) {
                    return false;
                }
                hasE = true;
                // 将其他三个 flg 置为 false,继续从 E 往后找新的数字
                hasNum = false;
                hasSign = false;
                hasDot = false;
            } else if (c == '+' || c == '-') {
                // 如果之前出现过符号或者数字或者'.',则返回 false
                if (hasSign || hasNum || hasDot) {
                    return false;
                }
                hasSign = true;
            } else if (c == '.') {
                // 如果之前出现过点或者 eE
                if (hasDot || hasE) {
                    return false;
                }
                hasDot = true;
            } else if (c == ' ') {
                // 说明此时可能到了字符串末尾的空格,也可能是夹在字符间的空格,需要额外处理。
                break;
            } else {
                // 其他非法字符
                return false;
            }
            index++;
        }
        // 处理末尾空格
        while (index < len && s.charAt(index) == ' ') {
            index++;
        }
        if (index == len && hasNum) {
            return true;
        }
        return false;
    }

    public boolean isNum(char c) {
        if (c >= '0' && c <= '9') return true;
        return false;
    }
}

24. LCR 157. 套餐内商品的排列顺序

这道题可以用深搜来做,还是用那一套模板,定义 flg 数组表示对应的 str[j] 是否被遍历过,注意去重可以用 是否是从左往右第一个未被填入的字符来判断( j > 0 && flg[j - 1] = false && flg[j - 1] == flg[j]),所以需要先对数组 str 进行排序,再从 0 下标开始遍历,如果该字符已经遍历过,或者不是从左往右第一个未被填入的字符,那就 continue,否则就将该字符 append 进 tmp 中,然后继续生成下一个位置的字符,然后就是回溯,将 flg[j] = false,并且将 str[j] 从 tmp 中删除。递归的出口就是当要生成的字符位置为 str.length 时,则说明字符生成完毕,将 tmp 添加进结果集合中,然后返回。

代码实现:

class Solution {
    List<String> arr;
    boolean[] flg;

    public String[] goodsOrder(String goods) {
        arr = new ArrayList<>();
        int len = goods.length();
        flg = new boolean[len];
        // 用深搜 + 回溯
        char[] str = goods.toCharArray();
        Arrays.sort(str);
        StringBuilder tmp = new StringBuilder();
        dfs(str, 0, len, tmp);
        String[] ret = new String[arr.size()];
        for (int i = 0; i < ret.length; i++) {
            ret[i] = arr.get(i);
        }
        return ret;
    }

    public void dfs(char[] str, int i, int len, StringBuilder tmp) {
        if (i == len) {
            // 说明已经生成完 len 个字符了
            arr.add(tmp.toString());
            return;
        }
        // 从 0 开始生成,第 i 位置的字符一共有 len 中可能性
        for (int j = 0; j < len; j++) {
            // 判断是否已经生成过该字符, 是否是从左往右第一个未被填入的字符
            if (flg[j] || (j > 0 && !flg[j - 1] && str[j - 1] == str[j])) {
                continue;
            }
            tmp.append(str[j]);
            flg[j] = true;
            // 生成下一个字符
            dfs(str, i + 1, len, tmp);
            // 回溯
            tmp.deleteCharAt(tmp.length() - 1);
            flg[j] = false;
        }
    }
}

25. LCR 167. 招式拆解 I

可以用滑动窗口和哈希表来做,定义 count 来记录最长连续不重复字符,定义 left 和 right 两个指针指向 0,用 set 来记录连续不重复字符,如果 arr[right] 是不重复字符,则添加到 set 里,然后 right++,直到遇到重复字符,那就先判断 set 的大小与 count 谁大,如果 set 大,则更新 count,然后再让 left 跳过重复字符,right++ 跳过重复字符,往后开始找新的连续不重复字符,最后返回 count 即可。

代码实现:

class Solution {
    public int dismantlingAction(String arr) {
        if (arr == null || arr.length() == 0) return 0;
        // 感觉可以用滑动窗口来做
        int count = 1;
        int left = 0;
        int right = 0;
        int len = arr.length();
        // set 用来记录连续不重复的字符
        Set<Character> set = new HashSet<>();

        while (right < len) {
            // 如果 arr[right] 不在 set 里,说明它不是重复字符
            // 则将 arr[right] 添加进 set 中,并让 right 往后走
            while (right < len && !set.contains(arr.charAt(right))) {
                set.add(arr.charAt(right++));
            }
            if (right >= len) {
                break;
            }
            // 如果 arr[right] 是重复字符 x ,则让 left 一直往后走
            // 直到跳过这个重复字符,然后最后再将重复字符 x 添加进 set 中
            // right 继续往后找连续不重复字符
            if (set.contains(arr.charAt(right))) {
                // 判断更新
                if (set.size() > count) {
                    count = set.size();
                }
                // 出窗口
                char ch = arr.charAt(right);
                while (left < right && arr.charAt(left) != ch) {
                    set.remove(arr.charAt(left++));
                }
                // set.remove(arr.charAt(left++));
                // set.add(ch);
                // left++ 跳过该重复字符
                left++;
                // 从重复字符往后开始找新的连续不重复字符
                right++;
            }
        }
        // 还需判断一次,防止漏掉最后的连续不重复字符
        if (set.size() > count) count = set.size();
        return count;
    }
}

26. LCR 169. 招式拆解 II

这个简单,用哈希表来做就行,记录每个字符出现的次数,然后再次遍历字符串,第一个出现次数为 1 的字母就是结果,如果到最后都没出现,则返回空格。

代码实现:

class Solution {
    public char dismantlingAction(String arr) {
        int[] hash = new int[26];
        for (int i = 0; i < arr.length(); i++) {
            char ch = arr.charAt(i);
            hash[ch - 'a']++;
        }
        for (int i = 0; i < arr.length(); i++) {
            char ch = arr.charAt(i);
            if (hash[ch - 'a'] == 1) {
                return ch;
            }
        }
        char c = ' ';
        return c;
    }
}

27. LCR 181. 字符串中的单词反转

这个也简单,整体思路就是先将整个字符串逆序,然后再分别逆序每个单词,可以用双指针来做,i 为 单词的开始下标,end 为单词的结束下标的下一个,先让 i 一直跳过空格找到字母或数字,然后 end 从 i 开始,只要不是空格,end 就往后走,当 end 停下来时,end - 1 就是单词结尾,逆序 i 到 end - 1 范围的字符,然后 i 再从 end 开始走。最后 i 重新遍历字符串数组,不是空格就一直添加进 ret 中,是空格就跳过,然后判断 i 是否越界,越界就跳出循环,然后如果 ret 不为空就添加一个空格到 ret 中,最后返回 ret.toString() 即可。

代码实现:

class Solution {
    public String reverseMessage(String message) {
        char[] arr = message.toCharArray();
        // 逆序整个字符串
        reverse(arr, 0, arr.length - 1);
        // 每个单词逆序
        int i = 0;
        while (i < arr.length) {
            // 跳过空格
            while (i < arr.length && arr[i] == ' ') {
                i++;
            }
            // 到这里 i 一定是数字或者字母
            int end = i;
            while (end < arr.length && arr[end] != ' ') {
                end++;
            }
            // 到这里 end 一定是空格,i 到 end - 1 则是单词
            reverse(arr, i, end - 1);
            i = end;
        }
        StringBuilder str = new StringBuilder();
        for (i = 0; i < arr.length; ) {
            while (i < arr.length && arr[i] != ' ') {
                str.append(arr[i++]);
            }
            while (i < arr.length && arr[i] == ' ') {
                i++;
            }
            // 防止句尾添加多余空格
            if (i >= arr.length) {
                break;
            }
            // 防止开头添加空格
            if (str.length() != 0) {
                str.append(' ');
            }
        }

        return str.toString();

    }

    public void reverse(char[] arr, int left, int right) {
        while (left < right) {
            char tmp = arr[left];
            arr[left++] = arr[right];
            arr[right--] = tmp;
        }
    }
}

28. LCR 182. 动态口令

这个也很简单,直接使用 substring 方法,截取字符串,最后拼接返回即可。

要是不让使用 substring 方法,那就自己遍历字符串即可。这个还可以简化一下,通过求余运算来简化。还有另一种方法,那就是先逆序整个字符串,然后再分别逆序 (0,len - t)  和 (len - t ,len)。

代码实现:

class Solution {
    public String dynamicPassword(String password, int target) {
        String str2 = password.substring(0, target);
        String str1 = password.substring(target);
        String ret = str1 + str2;
        return ret;
    }
}


class Solution {
    public String dynamicPassword(String password, int target) {
        StringBuilder ret = new StringBuilder();
        for(int i = target; i < password.length(); i++){
            ret.append(password.charAt(i));
        }
        for(int i = 0; i < target; i++){
            ret.append(password.charAt(i));
        }
        return ret.toString();
    }
}
// 简化后
class Solution {
    public String dynamicPassword(String password, int target) {
        StringBuilder ret = new StringBuilder();
        for(int i = target; i < password.length() + target; i++){
            ret.append(password.charAt(i % password.length()));
        }
        return ret.toString();
    }
}


// 逆序
class Solution {
    public String dynamicPassword(String password, int t) {
        // a b c d e  t = 2,len = 5
        // c d e a b  (2)
        // e d c b a  (3)
        // 要从 3 变到 2,需要逆序   (0,len - t)  和 (len - t, len)
        char[] arr = password.toCharArray();
        int len = arr.length;
        reverse(arr, 0, len - 1);
        reverse(arr, 0, len - 1 - t);
        reverse(arr, len - t, len - 1);
        return new String(arr);
    }

    public void reverse(char[] arr, int left, int right) {
        while (left < right) {
            char tmp = arr[left];
            arr[left++] = arr[right];
            arr[right--] = tmp;
        }
    }
}

29. LCR 192. 把字符串转换成整数 (atoi)

直接严格按照题目要求来写,

1. 首先跳过字符串的前导空格

2. 然后看下一个字符,是不是正号,负号

3. 然后再从下一个字符开始,一直读数字字符,如果该字符不是数字字符,就直接返回

4. 将上面读到的数字串转化为 int,且跳过前导 0。

5. 如果最后数字大于 int 最大值,返回 int 最大值,如果数字小于 int 最小值,返回 int 最小值,如果没有读到数字,返回 0。

代码实现:

class Solution {
    public int myAtoi(String str) {
        int i = 0;
        int len = str.length();
        // 是否是负数
        boolean flg = false;
        // 记录正负号数量
        int flgOp = 0;
        // 记录数字串
        StringBuilder num = new StringBuilder();

        // 跳过前导空格
        while (i < len && str.charAt(i) == ' ') {
            i++;
        }
        // 下一个字符是否是正负号,如果正负号或点号大于等于 2,则返回 0
        if (i < len && str.charAt(i) == '-') {
            flgOp++;
            flg = true;
            i++;
        }
        if (i < len && str.charAt(i) == '+') {
            flgOp++;
            i++;
        }

        // 如果该字符不是数字,则返回 0
        if (i < len && !isNum(str.charAt(i))) {
            return 0;
        }
        // 记录 从 i 开始一直到非数字的整个数字范围
        int end = i;
        while (end < len && isNum(str.charAt(end))) {
            end++;
        }
        String tmp = str.substring(i, end);
        // 添加进数字字符串中
        num.append(tmp);
        // 到这里就是非数字部分,直接退出
        // 正负号数量大于 1,则说明字符串不合法
        if (flgOp >= 2) {
            return 0;
        }

        double sum = 0;
        // 用 i 遍历 num 数字串
        i = 0;
        // 根据 num 字符串,计算结果
        // 跳过前导 0
        while (i < num.length() && num.charAt(i) == '0') {
            i++;
        }
        while (i < num.length()) {
            int t = num.charAt(i) - '0';
            sum = (sum * 10 + t);
            i++;
        }
        // 如果是负数
        if (flg) {
            sum *= -1;
        }

        if (sum > Integer.MAX_VALUE - 1) {
            return Integer.MAX_VALUE;
        } else if (sum < Integer.MIN_VALUE) {
            return Integer.MIN_VALUE;
        } else {
            return (int) sum;
        }

    }

    public boolean isNum(char c) {
        return c >= '0' && c <= '9';
    }
}

链表

30. LCR 123. 图书整理 I

这个简单,直接翻转链表,然后添加到数组中即可。

还可以用递归实现。

代码实现:

class Solution {
    public int[] reverseBookList(ListNode head) {
        if (head == null) return new int[0];
        // 头插法
        ListNode prev = head;
        ListNode cur = head.next;
        prev.next = null;
        int len = 1;
        while (cur != null) {
            ListNode curNext = cur.next;
            cur.next = prev;
            prev = cur;
            cur = curNext;
            len++;
        }
        int[] ret = new int[len];
        int i = 0;
        cur = prev;
        while (cur != null) {
            ret[i++] = cur.val;
            cur = cur.next;
        }
        return ret;
    }
}


// 递归
class Solution {
    public int[] reverseBookList(ListNode head) {
        getList(head);
        int[] ret = new int[arr.size()];
        for (int i = 0; i < ret.length; i++) {
            ret[i] = arr.get(i);
        }
        return ret;

    }

    public List<Integer> arr = new ArrayList<>();

    public void getList(ListNode cur) {
        if (cur == null) return;
        getList(cur.next);
        arr.add(cur.val);
    }
}

31. LCR 136. 删除链表的节点

这道题很简单,只要删除第一次出现的 val 元素即可,定义两个指针 prev 和 cur,prev 指向 cur 的前一个元素,cur 遍历链表,如果此时 cur.val == val,那就删除 cur,然后直接返回头节点,否则就让 prev 和 head 一直往后走,到最后,如果 head.val == val,那就返回 head.next 即可。

代码实现: 

class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        if (head == null) return null;
        ListNode prev = head;
        ListNode cur = head.next;
        while (cur != null) {
            if (cur.val == val) {
                prev.next = cur.next;
                cur = cur.next;
                return head;
            } else {
                prev = cur;
                cur = cur.next;
            }
        }

        if (head.val == val) head = head.next;
        return head;
    }
}

32. LCR 140. 训练计划 II

这个可以用快慢指针来做,定义 slow 和 fast 指向 head,先让 fast 走 cnt - 1 步,然后再让 fast 和 slow 一起走,当 fast 走到链表最后一个节点的位置时,slow 就是倒数第 cnt 个节点的位置。

代码实现:

class Solution {
    public ListNode trainingPlan(ListNode head, int cnt) {
        ListNode fast = head;
        ListNode slow = head;
        while (cnt - 1 != 0) {
            fast = fast.next;
            if (fast == null) {
                return null;
            }
            cnt--;
        }

        while (fast.next != null) {
            fast = fast.next;
            slow = slow.next;
        }
        return slow;

    }
}

33. LCR 141. 训练计划 III

这个简单,直接逆序链表,采用头插法即可。

代码实现:

class Solution {
    public ListNode trainningPlan(ListNode head) {
        // 采用头插法即可
        if (head == null || head.next == null) return head;
        ListNode prev = head;
        ListNode cur = head.next;
        // 防止代码成环
        prev.next = null;
        while (cur != null) {
            ListNode curNext = cur.next;
            cur.next = prev;
            prev = cur;
            cur = curNext;
        }
        return prev;

    }
}

34. LCR 142. 训练计划 IV

思路跟合并有序链表大差不差。

代码实现:

class Solution {
    public ListNode trainningPlan(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        // 虚拟头节点,返回时返回 dummy.next
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;
        // 其实跟合并有序数组差不多
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
                cur = cur.next;
            } else {
                cur.next = l2;
                l2 = l2.next;
                cur = cur.next;
            }
        }
        if (l1 != null) {
            cur.next = l1;
        }
        if (l2 != null) {
            cur.next = l2;
        }
        return dummy.next;
    }
}

35. LCR 171. 训练计划 V

找链表相交节点,这个可以用快慢指针来做,先让 fast 走差值步,然后 fast 和 slow 一起走,当 fast 和 slow 相遇时,就是链表相交节点。

代码实现:

class Solution {
    ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;
        int countA = 0;
        int countB = 0;
        ListNode fast = headA;
        ListNode slow = headB;
        while (fast != null) {
            fast = fast.next;
            countA++;
        }
        while (slow != null) {
            slow = slow.next;
            countB++;
        }
        fast = headA;
        slow = headB;
        int len = countA - countB;
        if (len < 0) {
            len = countB - countA;
            fast = headB;
            slow = headA;
        }
        while (len != 0) {
            fast = fast.next;
            len--;
        }
        while (fast != slow) {
            fast = fast.next;
            slow = slow.next;
        }
        return fast;

    }
}

36. LCR 154. 复杂链表的复制

让你复制一份链表,可以用哈希表来做,将老节点与新节点的对应关系存入哈希表中,最后重新遍历链表,根据哈希表来获取新节点,然后建立联系即可。

代码实现:

class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) return null;
        Node dummy = new Node(-1);
        Node cur = head;
        Map<Node, Node> hash = new HashMap<>();
        // 将老节点与新节点的关系存起来
        while (cur != null) {
            Node node = new Node(cur.val);
            hash.put(cur, node);
            cur = cur.next;
        }
        cur = head;
        while (cur != null) {
            hash.get(cur).next = hash.get(cur.next);
            hash.get(cur).random = hash.get(cur.random);
            cur = cur.next;
        }

        return hash.get(head);

    }
}

栈和队列

37. LCR 147. 最小栈

可以用两个栈来实现,一个栈当做正常的栈,另一个栈来存储最小值的栈。

入栈时,正常栈正常入栈,如果最小栈为空,那么也入栈,如果不为空,那么入栈的值 val 就需要与最小栈的栈顶元素比较,如果小于等于栈顶元素,那么 val 就入最小栈。

出栈时,首先需要判断两个栈是否为空,如果两个栈都为空,则不能出栈,否则正常栈正常出栈即可。获取栈顶元素也同理。

如果是获取最小栈栈顶元素,那还是得先判断最小栈是否为空,为空的话返回 -1,如果不为空,直接返回最小栈.peek() 即可。

代码实现:

class MinStack {
    // 可以用一个栈当正常栈
    // 一个栈当最小栈
    Stack<Integer> s1 = new Stack<>();
    Stack<Integer> s2 = new Stack<>();


    /**
     * initialize your data structure here.
     */
    public MinStack() {
        s1 = new Stack<>();
        s2 = new Stack<>();
    }

    public void push(int x) {
        s1.push(x);
        if (s2.empty()) {
            s2.push(x);
        } else {
            int top = s2.peek();
            if (x <= top) {
                s2.push(x);
            }
        }
    }

    public void pop() {
        if (s1.empty()) {
            return;
        }
        int top = s1.pop();
        if (!s2.empty() && s2.peek() == top) {
            s2.pop();
        }

    }

    public int top() {
        if (s1.empty()) {
            return -1;
        }
        return s1.peek();
    }

    public int getMin() {
        if (s2.empty()) {
            return -1;
        }
        return s2.peek();
    }
}

38. LCR 125. 图书整理 II

简单来说,就是让你用栈实现队列,那这个也简单,大体思路就是:直接定义两个栈,固定一个栈 1 入元素,另一个栈 2 出元素,如果栈 2 为空,那么就把栈 1 的所有元素出栈,并入到栈 2 2中,此时 栈 2 再出栈。

代码实现:

class CQueue {
    // 用栈来实现队列
    Stack<Integer> s1 = new Stack<>();
    Stack<Integer> s2 = new Stack<>();


    public CQueue() {
        s1 = new Stack<>();
        s2 = new Stack<>();
    }

    public void appendTail(int value) {
        s1.push(value);
    }

    public int deleteHead() {
        if (s1.empty() && s2.empty()) {
            return -1;
        }
        if (s2.empty()) {
            while (!s1.empty()) {
                s2.push(s1.pop());
            }
        }
        return s2.pop();
    }
}

39. LCR 184. 设计自助结算系统

这道题的难点是如何用 O(1) 的时间获取的最大值,因为最大值的候补者有许多个,如何在删除最大值的时候,快速找到下一个最大值。我们可以用普通队列和双端队列实现,双端队列用来存储最大值的候补者,并将这些最大值的候补者按照降序来排序,那么双端队列的第一个元素就一定是最大值。而普通队列就正常进行操作就行,但是双端队列进行操作时就需要进行判断。

在新增元素时,双端队列如果为空,那么该元素可能是最大值,则从尾巴入队。如果双端队列不为空,就需要判断队列的尾巴元素与当前元素 val 比较,如果 val 比尾巴元素要大,则说明尾巴元素就一定不可能是最大值了,所以一直出队,直到遇到尾巴元素比当前元素 val 大,或者等于当前元素。那么此时当前元素就可以从尾巴入队了。

删除元素时,首先判断正常队列是否为空,如果为空,则直接返回,如果不为空,则正常队列出队得到了 top 元素,还需要去看看双端队列的头元素是否与 top 相同,如果相同,则双端队列把头元素出队。

代码实现:

class Checkout {
    // 可以用队列和双端队列实现
    Queue<Integer> queue;
    // 双端队列存储可能是最大值的元素,并且按照降序存储
    Deque<Integer> maxQueue;

    public Checkout() {
        queue = new LinkedList<>();
        maxQueue = new LinkedList<>();
    }

    public int get_max() {
        if (maxQueue.isEmpty()) {
            return -1;
        }
        // 取出双端队列的头元素就是最大值
        return maxQueue.peekFirst();

    }

    public void add(int value) {
        queue.offer(value);
        // 如果双端队列为空,则当前元素可能是最大值,入队
        if (maxQueue.isEmpty()) {
            maxQueue.offerLast(value);
            return;
        }
        // 如果不为空,则需要判断,如果当前 val 大于双端队列的尾巴元素,
        // 说明此时双端队列的尾巴元素一定不可能是最大值,要保持最大值降序排序
        // 那就把双端队列的尾巴元素删掉,然后把 val 添加进来
        while (!maxQueue.isEmpty() && value > maxQueue.peekLast()) {
            maxQueue.pollLast();
        }
        maxQueue.offerLast(value);
    }

    public int remove() {
        if (queue.isEmpty()) {
            return -1;
        }
        // 删除时也同理,需要判断删除的元素是否与 max 的头元素相同,
        // 如果相同,则 max 也需要出队
        int top = queue.poll();
        if (top == maxQueue.peekFirst()) {
            maxQueue.pollFirst();
        }
        return top;
    }
}

40. LCR 148. 验证图书取出顺序

这道题不难,就是遍历入栈数组,先将里面的元素入栈,然后判断栈顶元素与出栈元素是否相同,如果是,那就出栈,并且用 j 来遍历出栈数组,j++。

如果最后栈里为空,则返回 true,否则返回 false。

代码实现:

class Solution {
    public boolean validateBookSequences(int[] putIn, int[] takeOut) {
        Stack<Integer> stack = new Stack<>();
        int j = 0;
        for (int i = 0; i < putIn.length; i++) {
            stack.push(putIn[i]);
            while (!stack.empty() && stack.peek() == takeOut[j]) {
                stack.pop();
                j++;
            }
        }
        if (stack.empty()) {
            return true;
        }
        return false;
    }
}

41. LCR 124. 推理二叉树

代码实现:

class Solution {
    public int preIndex;

    public TreeNode deduceTree(int[] preorder, int[] inorder) {
        TreeNode root = createTree(inorder, preorder, 0, preorder.length - 1);
        return root;
    }

    public TreeNode createTree(int[] inorder, int[] preorder, int start, int end) {
        if (start > end) return null;
        TreeNode root = new TreeNode(preorder[preIndex]);
        // 找下标
        int rootIndex = getRootIndex(inorder, start, end, preorder[preIndex]);
        preIndex++;
        if (rootIndex == -1) return root;
        // 按照前序遍历来创建二叉树
        root.left = createTree(inorder, preorder, start, rootIndex - 1);
        root.right = createTree(inorder, preorder, rootIndex + 1, end);
        return root;
    }

    // 在中序遍历中找到根节点
    public int getRootIndex(int[] inorder, int start, int end, int key) {
        for (int i = start; i <= end; i++) {
            if (inorder[i] == key) {
                return i;
            }
        }
        return -1;
    }
}

42. LCR 143. 子结构判断

这道题思路跟找子树差不多,区别就是找子树时,树 1 和树 2 的结构必须完全相同,并且对应的值也必须相同,但是这里如图例 2 ,这里当树 2 为空时,树 1 可以不为空。所以在写找子树代码时还可以分以下情况,修改修改。

总体思路就是,找子树,子树要结构相同,并且值相同,左子树相同并且右子树相同才是子树,可以用前序遍历来做,每遍历到一个节点,都去看看当前节点是否就是子树。但是得注意,当两棵树都为空时,要返回 false。

代码实现: 

class Solution {
    public boolean isSubStructure(TreeNode A, TreeNode B) {
        // 题目要求,如果两棵树都为空,则返回 false
        if (A == null && B == null) {
            return false;
        }
        if (A == null && B != null || A != null && B == null) return false;

        return isSubTree(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
    }

    public boolean isSubTree(TreeNode p, TreeNode q) {
        // 1. 值相同  2. 结构相同
        if (p == null && q == null) return true;

        if (p == null && q != null) return false;

        // 当 q 为空时,p 可以不为空
        if (p != null && q == null) return true;

        // 值不相同
        if (p.val != q.val) return false;

        return isSubTree(p.left, q.left) && isSubTree(p.right, q.right);
    }
}

43. LCR 144. 翻转二叉树

这个很简单,就是左子树跟右子树交换即可。用前序遍历,每一个节点的左孩子和右孩子交换即可。

代码实现:

class Solution {
    public TreeNode mirrorTree(TreeNode root) {
        if (root == null) return null;
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
        mirrorTree(root.left);
        mirrorTree(root.right);
        return root;
    }
}

44. LCR 145. 判断对称二叉树

这道题就是判断二叉树是否对称,前提条件就是得判断结构对称,并且值也得对称,并且左孩子 1的左孩子 2 和右孩子 1 的右孩子 2 对称,并且左孩子 1 的右孩子 2 与右孩子 1 的左孩子 2 对称。

代码实现:

class Solution {
    public boolean checkSymmetricTree(TreeNode root) {
        if (root == null) return true;
        return isSymmetricTree(root.left, root.right);
    }

    public boolean isSymmetricTree(TreeNode p, TreeNode q) {
        // 1. 结构相同 2. 值相同
        if (p == null && q == null) return true;
        if (p == null && q != null || p != null && q == null) return false;
        if (p.val != q.val) return false;
        // 对称,如图示例一
        return isSymmetricTree(p.left, q.right) && isSymmetricTree(p.right, q.left);
    }
}

45. LCR 149. 彩灯装饰记录 I

就是二叉树层序遍历,利用队列实现即可,如果队列不为空,那就先出队 top,如果左子树不为空,那就将左子树入队,如果右子树不为空,那就将右子树入队,然后再将 top 添加到数组中,最后返回数组即可。

代码实现:

class Solution {
    public int[] decorateRecord(TreeNode root) {
        if (root == null) return new int[0];
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        List<Integer> arr = new ArrayList<>();
        while (!queue.isEmpty()) {
            TreeNode top = queue.poll();
            arr.add(top.val);
            if (top.left != null) {
                queue.offer(top.left);
            }
            if (top.right != null) {
                queue.offer(top.right);
            }
        }
        int[] ret = new int[arr.size()];
        for (int i = 0; i < ret.length; i++) {
            ret[i] = arr.get(i);
        }
        return ret;
    }
}

46. LCR 150. 彩灯装饰记录 II

与上一题代码基本一致,就是在进入循环时,判断队列中有多少元素 size,队列中 size 的个数就是这一层节点的个数,然后循环出队 size 次,就把这层节点元素全部拿到了,最后添加进数组中,然后返回数组即可。

代码实现:

class Solution {
    public List<List<Integer>> decorateRecord(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<>();
        if (root == null) return ret;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            List<Integer> arr = new ArrayList<>();
            int size = queue.size();
            while (size != 0) {
                TreeNode top = queue.poll();
                arr.add(top.val);
                if (top.left != null) {
                    queue.offer(top.left);
                }
                if (top.right != null) {
                    queue.offer(top.right);
                }
                size--;
            }
            ret.add(arr);
        }

        return ret;
    }
}

47. LCR 151. 彩灯装饰记录 III

跟层序遍历差不多,就是逆转了下双数层的值的顺序,可以利用 Collections.reverse 方法来完成。

代码实现:

class Solution {
    public List<List<Integer>> decorateRecord(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<>();
        if (root == null) return ret;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        boolean flg = true;
        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> arr = new ArrayList<>();
            while (size != 0) {
                TreeNode top = queue.poll();
                arr.add(top.val);

                if (top.left != null) {
                    queue.add(top.left);
                }
                if (top.right != null) {
                    queue.add(top.right);
                }
                size--;
            }
            if (!flg) {
                Collections.reverse(arr);
            }
            flg = !flg;
            ret.add(arr);
        }
        return ret;
    }
}

48. LCR 152. 验证二叉搜索树的后序遍历序列

二叉搜索树就是左子树小于根节点,右子树大于根节点,所以只需要判断通过递归来判定所有节点都符合上面的要求,那么这棵树就是二叉搜索树。先划分左右子树的区间,找到第一个右子树的节点(节点值大于根节点),然后再判断右子树区间的值是否全都比根节点大,如果不是,返回 false,如果是,则递归去判断左子树和右子树。

代码实现:

class Solution {
    public boolean verifyTreeOrder(int[] postorder) {
        return verifyTreeOrder(postorder, 0, postorder.length - 1);
    }

    public boolean verifyTreeOrder(int[] postorder, int start, int end) {
        if (start >= end) return true;
        // 以 end 为根节点,划分左子树和右子树
        int root = postorder[end];
        // 往后找大于 root 的值, 找到第一个右子树节点
        int cur = start;
        while (postorder[cur] < postorder[end]) cur++;
        // 此时 postorder[cur] 一定大于 root
        int mid = cur;
        // mid, end 就是右子树范围
        while (postorder[cur] > postorder[end]) cur++;
        // 到这里,start, mid - 1 是左子树,  m,end - 1 是右子树
        return cur == end && verifyTreeOrder(postorder, start, mid - 1) && verifyTreeOrder(postorder, mid, end - 1);
    }
}

49. LCR 153. 二叉树中和为目标值的路径

可以用深搜来做,定义一个数组 arr 来存放遍历的节点,定义一个 sum 用来记录当前遍历过所有节点的和,用前序遍历去遍历每一个节点,先将 root 的值添加进 arr 和 sum 中,如果 sum 等于 target 并且此时是叶子节点,就添加进返回结果中,然后再删除数组 arr 当前节点,sum 减等当前节点值,最后返回结果,其他情况就继续遍历左子树,并将遍历的结果添加进返回结果中,右子树同理,最后再删除 arr 里当前节点值,sum 减去当前节点值,最后再返回结果即可。

代码实现:

class Solution {

    public List<List<Integer>> pathTarget(TreeNode root, int target) {
        dfs(root, target);
        return ret;

    }

    public int sum = 0;
    public List<Integer> arr = new ArrayList<>();
    public List<List<Integer>> ret = new ArrayList<>();

    public void dfs(TreeNode root, int target) {
        if (root == null) return;
        arr.add(root.val);
        sum += root.val;
        if (sum == target && root.left == null && root.right == null) {
            ret.add(new ArrayList<>(arr));
            arr.remove(arr.size() - 1);
            sum -= root.val;
            return;
        }

        dfs(root.left, target);
        dfs(root.right, target);
        arr.remove(arr.size() - 1);
        sum -= root.val;

    }
}

50. LCR 155. 将二叉搜索树转化为排序的双向链表

将二叉搜索树转化为排序的双向链表,可以利用二叉搜索树的中序遍历是有序的特点,可以定义 cur 和 prev 两个指针,prev 是 cur 的中序遍历的前一个节点,先递归往 cur 的右边走,然后判断 prev 是否为空,如果为空,说明此时 cur 就是链表头节点,记录一下。如果不为空,就可以进行修改指向了,prev.right = cur。 cur.left = prev,然后再让 prev 往后走,继续递归 cur 的右边。递归结束后,prev 就是链表的尾巴节点,只需要修改一下链表头节点和尾巴节点的指向,最后再返回即可。

代码实现:  

class Solution {
    public Node treeToDoublyList(Node root) {
        if (root == null) return null;
        dfs(root);
        // 最后 prev 一定就是尾巴节点
        head.left = prev;
        prev.right = head;
        return head;
    }

    public Node prev = null;
    public Node head = null;

    // 中序遍历
    public void dfs(Node cur) {
        if (cur == null) return;
        dfs(cur.left);
        if (prev == null) {
            head = cur;
        } else {
            prev.right = cur;
        }
        cur.left = prev;
        // prev 往后走
        prev = cur;
        dfs(cur.right);
    }
}

51. LCR 156. 序列化与反序列化二叉树

序列化就是将二叉树转化成字符串,反序列化就是将字符串转化成二叉树,这道题可以用层序遍历来做,在序列化时,可以将节点为空的情况也添加到字符串中,每个节点以逗号分隔。

可以从第一层节点开始,从左到右从上到下都编一个号,从 0 开始编号,那么当前节点 cur 编号为 i,则 2*i + 1 是 cur 的左孩子,2*i + 2 是 cur 右孩子,根据这个来进行反序列化,因为是按照层序遍历的方式序列化的,所以也是按照层序遍历的方式来反序列化,将创建的节点都添加到队列中,先按照 "," 来分隔字符串,这样每个字符串 str[i] 就都是一个节点,i 从 0 开始,如果队列不为空,就出队一个元素 top,如果 top 不为空,说明 top 是一个节点,就说明 top 可能会有左右孩子,就分别判断 2*i + 1 和 2*i + 2 是否越界,如果没有越界,并且对应的 str[2*i + 1] 不为空和 str[2*i + 2] 不为空,则说明存在左孩子或者右孩子,则将左孩子和右孩子添加进队列中,并让 top 的 left 和 right 分别指向左右孩子。然后 i++ 往后走,最后返回 root 即可。

代码实现:

public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if (root == null) return "[]";
        // 可以利用层序遍历实现
        Queue<TreeNode> queue = new LinkedList<>();
        StringBuilder str = new StringBuilder("[");
        queue.offer(root);
        while (!queue.isEmpty()) {
            TreeNode top = queue.poll();
            if (top != null) {
                str.append(top.val + ",");
                queue.offer(top.left);
                queue.offer(top.right);
            } else
                str.append("null,");

        }
        str.deleteCharAt(str.length() - 1);
        str.append("]");
        return str.toString();

    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if (data.equals("[]")) {
            return null;
        }
        // 点号分隔,那就是每个节点 [1,2,3,null,null,4,5,null,null]

        String[] arr = data.substring(1, data.length() - 1).split(",");
        TreeNode root = new TreeNode(Integer.valueOf(arr[0]));
        // 用层序遍历的方式解析
        // 从 0 下标开始编号,如果该节点不为空,则:
        // 左子树 2*i + 1
        // 右子树 2*i + 2

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int i = 0;
        while (!queue.isEmpty()) {
            TreeNode top = queue.poll();
            if (2 * i + 1 < arr.length && !arr[2 * i + 1].equals("null")) {
                TreeNode left = new TreeNode(Integer.valueOf(arr[2 * i + 1]));
                queue.offer(left);
                top.left = left;
            }
            if (2 * i + 2 < arr.length && !arr[2 * i + 2].equals("null")) {
                TreeNode right = new TreeNode(Integer.valueOf(arr[2 * i + 2]));
                queue.offer(right);
                top.right = right;
            }
            // i++ 去遍历后一个节点
            i++;

        }
        return root;
    }
}

52. LCR 174. 寻找二叉搜索树中的目标节点

一眼看到求第 k 大元素,那就可以用优先级队列来解决,创建小根堆,用前序遍历,当小堆元素个数小于 cnt 时,那就入堆,如果元素个数大于等于 cnt,那就判断一下 root.val 与堆顶元素谁大,如果 root.val 大,那么小堆出队,然后将 root.val 添加进小堆中,然后继续遍历左子树和右子树。

遍历完成后,小堆的堆顶元素就是第 cnt 大元素。

代码实现:

class Solution {
    public int findTargetNode(TreeNode root, int cnt) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        getKMax(root, cnt, queue);
        return queue.peek();
    }

    public int i;

    public void getKMax(TreeNode root, int cnt, PriorityQueue<Integer> queue) {
        if (root == null) return;
        if (i < cnt) {
            queue.offer(root.val);
        } else {
            int top = queue.peek();
            int val = root.val;
            if (val > top) {
                queue.poll();
                queue.offer(val);
            }
        }
        i++;
        getKMax(root.left, cnt, queue);
        getKMax(root.right, cnt, queue);
    }
}

看了大佬的题解后,发现可以利用二叉搜索树的中序遍历是有序的来做,求第 K 大元素,那我们就可以中序遍历反着遍历,此时遇到的第 K 个元素就是第 K 大元素。

代码实现:

class Solution {
    public int findTargetNode(TreeNode root, int cnt) {
        getMax(root, cnt);
        return kMax;
    }

    public int i = 1;
    public int kMax = 0;

    public void getMax(TreeNode root, int cnt) {
        if (root == null) return;
        // 中序:  左根右
        // 反着来:右根左
        getMax(root.right, cnt);
        if (i == cnt) {
            kMax = root.val;
        }
        i++;
        if (i > cnt) return;
        getMax(root.left, cnt);

    }
}

53. LCR 175. 计算二叉树的深度

这个很简单,让你计算二叉搜索树的高度,可以用递归来做,整棵树的高度就等于左子树高度和右子树的高度的最大值,再加上自己这个节点本身,也就是 +1 。

代码实现:

class Solution {
    public int calculateDepth(TreeNode root) {
        if (root == null) return 0;
        int leftHeight = calculateDepth(root.left);
        int rightHeight = calculateDepth(root.right);
        return Math.max(leftHeight, rightHeight) + 1;
    }
}

54. LCR 176. 判断是否为平衡二叉树

这个也很简单,可以在计算树的高度的时候,去判断是否是平衡二叉树。如果是,则正常返回高度,如果不是,则返回 -1 即可,最后只需要判断得到的高度是否大于等于 0。 

代码实现:

class Solution {
    public boolean isBalanced(TreeNode root) {
        // 可以在计算树的高度时,也判断是否是平衡二叉树
        return getHeight(root) >= 0;
    }


    public int getHeight(TreeNode root) {
        if (root == null) return 0;
        int leftHeight = getHeight(root.left);
        int rightHeight = getHeight(root.right);
        if (leftHeight >= 0 && rightHeight >= 0 && Math.abs(leftHeight - rightHeight) <= 1) {
            return Math.max(leftHeight, rightHeight) + 1;
        } else {
            // 说明不是平衡二叉树,那高度返回负数即可
            return -1;
        }
    }
}

55. LCR 193. 二叉搜索树的最近公共祖先

这道题得分情况讨论,第一种情况是 p == root 或者 q == root,则此时最近公共祖先就是 root

第二种情况是如果 p 和 q 都在 root 的左边,或者 p 和 q 都在 root 的右边,如例子2,就看谁在前头就是谁。

第三种情况就是 p 和 q 分别在 root 的两边,那此时这种情况最近公共祖先就是 root,如例子1。

代码实现:

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        // 分三种情况,第一种是 p 或者 q 中有一个是 root
        // 一种是 p 和 q 都在 root 的左边或者右边
        // 另一种是 p 和 q 分别在 root 的两边
        if (root == p || root == q) return root;
        if (root == null || p == null || q == null) return null;
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        if (left != null && right != null) {
            return root;
        } else if (left == null) {
            return right;
        } else {
            return left;
        }

    }
}

56. LCR 194. 二叉树的最近公共祖先

这道题和上面那道题是解法一模一样,还有另一种做法。

如图:

代码实现:

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null) return null;
        Stack<TreeNode> s1 = new Stack<>();
        Stack<TreeNode> s2 = new Stack<>();
        getPath(root, p, s1);
        getPath(root, q, s2);
        int len = s1.size() - s2.size();
        if (len < 0) {
            len = s2.size() - s1.size();
            while (len != 0) {
                s2.pop();
                len--;
            }
        } else {
            while (len != 0) {
                s1.pop();
                len--;
            }
        }
        while (!s1.empty() && !s2.empty()) {
            TreeNode top1 = s1.pop();
            TreeNode top2 = s2.pop();
            if (top1 == top2) return top1;
        }
        return null;
    }

    public boolean getPath(TreeNode root, TreeNode target, Stack<TreeNode> stack) {
        if (root == null) return false;
        stack.push(root);
        if (root == target) return true;
        boolean left = getPath(root.left, target, stack);
        if (left) return true;
        boolean right = getPath(root.right, target, stack);
        if (right) return true;
        // 到这里说明左子树和右子树都没找到 target
        // 说明不是路径上的节点, 删除节点返回 false
        stack.pop();
        return false;
    }
}

位运算

57. LCR 133. 位 1 的个数

这个简单,直接利用 n &= (n - 1) 消除 n 中二进制 1 的个数,并且用计数器记录下来即可。

代码实现:

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int count = 0;
        while (n != 0) {
            n &= (n - 1);
            count++;
        }

        return count;

    }
}

58. LCR 134. Pow(x, n)

可以用快速幂来做,就是 x^n = (x*x)^(n/2)(n 为偶数)或者 x*(x*x)^(n/2)

那就可以利用这个来做题。

代码实现:

class Solution {
    public double myPow(double x, int n) {
        if (x == 0.0) return 0.0;
        // 用快速幂
        // 2^9 = (2)* 4^4 = (2)* 16^2 = (2)* 256^1 = (2*256)* (256*256)^0 = 512
        // 用来记录多出来的 x,也就是当 n 为奇数时,res *= x
        double res = 1.0;
        boolean flg = false;
        // n 可能会很大,将 n 赋给 tmp 防止越界(-2147483648)
        long tmp = n;
        if (tmp < 0) {
            tmp = -tmp;
            flg = true;
        }
        while (tmp != 0) {
            if (tmp % 2 == 1) {
                res *= x;
            }
            x = x * x;
            tmp /= 2;

        }
        if (flg) {
            return 1.0 / res;
        }
        return res;
    }
}

59. LCR 177. 撞色搭配

这个可以用分组异或的方式来做,先将数组所有元素异或一遍 得到 ret,ret 中的二进制为 1 的那一位说明就是两个只出现一次的数字的区分点,然后根据这个区分点,来分组异或,最后返回异或得到的结果即可。

代码实现:

class Solution {
    public int[] sockCollocation(int[] sockets) {
        int tmp = 0;
        for (int i = 0; i < sockets.length; i++) {
            tmp ^= sockets[i];
        }
        // 分组异或
        int index = 0;
        while (tmp != 0) {
            if (tmp % 2 == 1) {
                break;
            }
            tmp >>= 1;
            index++;
        }

        int num1 = 0;
        int num2 = 0;
        for (int i = 0; i < sockets.length; i++) {
            if (((sockets[i] >> index) & 1) == 1) {
                num1 ^= sockets[i];
            } else {
                num2 ^= sockets[i];
            }
        }
        return new int[]{num1, num2};
    }
}

60. LCR 178. 训练计划 VI

这道题可以用哈希表来做,不会超时,遍历数组记录每个数字出现的次数,然后再次遍历数组,如果当前数字出现了一次,就返回该数字。

代码实现:

class Solution {
    public int trainingPlan(int[] actions) {
        Map<Integer, Integer> hash = new HashMap<>();
        for (int i = 0; i < actions.length; i++) {
            int value = hash.getOrDefault(actions[i], 0);
            hash.put(actions[i], value + 1);
        }
        for (int i = 0; i < actions.length; i++) {
            int value = hash.getOrDefault(actions[i], 0);
            if (value == 1) {
                return actions[i];
            }
        }
        return -1;
    }
}

61. LCR 190. 加密运算

这个就是让你求两个数的和,但是不能使用加减乘除,那就可以使用位运算来做,a ^ b 就是 a 和 b 不进位相加的和,那我们可以定义一个变量来记录 a 和 b 的进位,然后就一直进行异或直到进位为 0.

3 和 5

  011       3

  101       5

  001   a & b 这就是进位,需要往前进位, 001 -->  010,所以两数之和 = 不进位和 + 进位和

           所以进位的计算公式为 (a & b) << 1 

  010   (a & b) << 1 

  110   a ^ b    

  100   上面两个异或之后得到不进位和,再求进位

  100   (a & b) << 1   进位

0000   上面两个异或之后得到不进位和,再求进位

1000   (a & b) << 1   进位

1000   异或和

0000   进位,当进位为 0,上面两数之和计算完毕

代码实现:

class Solution {
    public int encryptionCalculate(int dataA, int dataB) {
        if (dataA == 0) return dataB;
        if (dataB == 0) return dataA;
        // 两个数不进位相加的结果  a ^ b

        while (dataB != 0) {
            int carry = (dataA & dataB) << 1; // 计算进位
            dataA = dataA ^ dataB;// 计算两数不进位相加和
            dataB = carry;
        }
        return dataA;
    }
}

动态规划

62. LCR 126. 斐波那契数

这个很简单,公式给你了,按照公式求解就行。

代码实现:

class Solution {
    public int fib(int n) {
        if (n == 0 || n == 1) return n;
        int a = 0;
        int b = 1;
        int c = 0; 

        while (n >= 2) {
            c = (a + b) % 1000000007;
            a = b;
            b = c;
            n--;
        }
        return c;

    }
}

63. LCR 127. 跳跃训练

这个很简单,就是青蛙跳台阶问题,跟斐波那契问题的解法一模一样。

代码实现:

class Solution {
    public int trainWays(int num) {
        if (num < 2) return 1;
        int[] dp = new int[num + 1];
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <= num; i++) {
            dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007;
        }
        return dp[num];
    }
}

64. LCR 137. 模糊搜索验证

这道题用动态规划来做,大体分两种情况,当 input 的第 j-1 个字符是 * 以及不是 * 的情况。

首先是明确 dp[i][j],根据题目可知表示以 article 的第 i 个字符结尾的串与以 input 的第 j 个字符结尾的串是否匹配。然后推导状态转移方程 dp[i][j] = ?

当 input[j - 1] != '*' 时,如果 article[i-1] == input[j-1] 或者有个字符是点号,则 dp[i][j] = dp[i - 1][j - 1] (看以 article 的第 i 个字符结尾的串与以 input 的第 j 个字符结尾的串的状态)

当 input[j - 1] 等于 '*' 时,dp[i][j] = dp[i][j-2] (因为 input[j-1] 是 '*', 所以关注 intput[j - 2] 结尾的串即可),如果 article[i-1] == input[j-2] 或者其中一个字符是'*' 或者点号,则 dp[i][j] = dp[i][j] || dp[i - 1][j]。初始化就是当两字符串都是空串时,dp[0][0] = true。

代码实现:

class Solution {
    public boolean articleMatch(String a, String b) {
        // 用动态规划来做,dp[i][j] 代表在 a 中以 i 个字符结尾的字符串与在 b 中第 j 个字符结尾的字符串是否匹配
        int len1 = a.length();
        int len2 = b.length();
        boolean[][] dp = new boolean[len1 + 1][len2 + 1];
        // 初始化
        dp[0][0] = true;
        char[] arr1 = a.toCharArray();
        char[] arr2 = b.toCharArray();
        for (int i = 0; i <= len1; i++) {
            for (int j = 1; j <= len2; j++) {
                if (arr2[j - 1] == '*') {
                    dp[i][j] = dp[i][j - 2];
                    if (i > 0) {
                        if (charMatch(arr1[i - 1], arr2[j - 2])) {
                            dp[i][j] = dp[i][j] || dp[i - 1][j];
                        }
                    }
                } else {
                    if (i > 0 && charMatch(arr1[i - 1], arr2[j - 1])) {
                        dp[i][j] = dp[i - 1][j - 1];
                    }
                }

            }
        }
        return dp[len1][len2];
    }

    public boolean charMatch(char a, char b) {
        if (a == '*' || b == '*' || a == '.' || b == '.') return true;
        return a == b;
    }
}

65. LCR 161. 连续天数的最高销售额

这道题可以用动态规划来做,求连续数组最大和,dp[i] 就是以 i 下标结尾天数的最大和。

dp[i] = Math.max(sales[i], dp[i - 1] + sales[i]),因为 sales[i] 可能是负数。

初始化,dp[0] = sales[0],最后遍历 dp 找最大值并返回即可。

代码实现:

class Solution {
    public int maxSales(int[] sales) {
        // dp[i] 表示以 i 下标结尾的天数的最大和
        int[] dp = new int[sales.length];
        dp[0] = sales[0];
        for (int i = 1; i < dp.length; i++) {
            dp[i] = Math.max(dp[i - 1] + sales[i], sales[i]);
        }
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < dp.length; i++) {
            if (dp[i] > max) {
                max = dp[i];
            }
        }
        return max;
    }
}

66. LCR 165. 解密数字

这道题也可以用动态规划来做,dp[i] 代表以第 i 个数字结尾的字符串有多少种解密方案。

 一个数字就代表一个字母,但是这个数字范围是 0-25。

所以根据数字,这个解密方式 dp[i] = dp[i - 1]。当 str[i - 1] 和 str[i] 能够组合数字时(num >= 10 && num <= 25),则 dp[i] = dp[i - 1] + dp[i - 2] 。

初始化,dp[0] = dp[1] = 1。那这里跟斐波那契问题有点像,就可以定义三个变量 a,b,c 来替代 dp 数组。

代码实现:

class Solution {
    public int crackNumber(int ciphertext) {
        String str = String.valueOf(ciphertext);
        // int[] dp = new int[str.length() + 1];
        // dp[i] 表示以第 i 个字符结尾的解密方案数量
        int a = 1, b = 1, c = 1;
        int len = str.length();
        int i = 2;
        while (i <= len) {
            c = b;
            // 如果 str[i - 2] 存在, 则可以组合 str[i - 2]str[i - 1]
            String tmp = str.substring(i - 2, i);
            if (tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0) {
                c += a;
            }
            a = b;
            b = c;
            i++;
        }
        return c;
    }
}

67. LCR 166. 珠宝的最高价值

可以用动态规划来做,先定义 dp[i][j] 为从 frame[0][0] 到 frame[i][j] 的珠宝最大值,然后再来推断dp[i][j] = ?,到达 dp[i][j] 有两种可能,第一种是从上面来到,第二种是从左边来的。

所以 dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + frame[i][j] 。但还要考虑边界情况比如在左上角和在右上角的情况,此时就只有一个方向。最后就是初始化 dp[0][0] = frame[0][0]

代码实现:

class Solution {
    public int jewelleryValue(int[][] frame) {
        int row = frame.length;
        int col = frame[0].length;
        int[][] dp = new int[row][col];
        dp[0][0] = frame[0][0];
        // dp[i][j] 代表到 i,j 的最大珠宝值
        // 1 3 1
        // 1 5 1
        // 4 2 1
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                dp[i][j] = frame[i][j];
                if (i - 1 >= 0) {
                    dp[i][j] = dp[i - 1][j] + frame[i][j];
                }
                if (j - 1 >= 0) {
                    dp[i][j] = Math.max(dp[i][j], dp[i][j - 1] + frame[i][j]);
                }

            }
        }
        return dp[row - 1][col - 1];
    }
}

68. LCR 185. 统计结果概率

这道题,可以去看看大佬的题解。

代码实现:

class Solution {
    public double[] statisticsProbability(int num) {
        // prev 表示 n - 1 个骰子点数
        double[] prev = {1 / 6d, 1 / 6d, 1 / 6d, 1 / 6d, 1 / 6d, 1 / 6d};
        for (int i = 2; i <= num; i++) {
            // 第 n 个骰子点数
            double[] cur = new double[5 * i + 1];
            // [n, 6n] -> 5*n + 1
            for (int j = 0; j < prev.length; j++) {
                // 每个骰子有 6 点
                // 第 n 个骰子点数 = 第 n-1 个骰子点数 + 当前骰子点数
                for (int k = 0; k < 6; k++) {
                    cur[j + k] += prev[j] * 1 / 6;
                }
            }
            prev = cur;
        }
        return prev;
    }
}

69. LCR 187. 破冰游戏

约瑟夫环问题,dp[i] = (dp[i -1] + target) % num.

代码实现:

class Solution {
    public int iceBreakingGame(int num, int target) {
        // 用动态规划
        // dp[i] = (dp[i - 1] + target) % num
        // dp[i] 表示以 i 个成员为约瑟夫环,最后剩下的成员编号
        int dp = 0;
        for (int i = 2; i <= num; i++) {
            dp = (dp + target) % i;
        }
        return dp;
    }
}

70. LCR 129. 字母迷宫

这种找路径的问题,一般用深搜来做,因为图中的每个节点都可能是单词开头,所以需要遍历图,然后对图中的每个节点进行深搜即可。

代码实现:

class Solution {
    public boolean wordPuzzle(char[][] grid, String target) {
        int row = grid.length;
        int col = grid[0].length;
        char[] arr = target.toCharArray();
        // 遍历图
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                boolean[][] flg = new boolean[row][col];
                // 对每个节点进行深搜
                if (dfs(grid, flg, i, j, row, col, arr, 0)) {
                    return true;
                }
            }
        }

        return false;
    }

    public boolean dfs(char[][] arr, boolean[][] flg, int i, int j, int row, int col, char[] target, int k) {
        // 越界或者已经访问过或者对应的字符不一致,则返回 false
        if (i < 0 || i >= row || j < 0 || j >= col || flg[i][j] || arr[i][j] != target[k]) {
            return false;
        }
        // 标记遍历过
        flg[i][j] = true;
    
        if (k == target.length - 1) {
            return true;
        }

        // 往四个方向继续
        boolean ans = dfs(arr, flg, i - 1, j, row, col, target, k + 1)
                || dfs(arr, flg, i + 1, j, row, col, target, k + 1)
                || dfs(arr, flg, i, j - 1, row, col, target, k + 1)
                || dfs(arr, flg, i, j + 1, row, col, target, k + 1);
        // 回溯
        flg[i][j] = false;
        return ans;
    }
}

71. LCR 130. 衣橱整理

可以用深搜来做。

代码实现:

class Solution {
    public int wardrobeFinishing(int m, int n, int cnt) {
        boolean[][] flg = new boolean[m][n];
        return dfs(m, n, 0, 0, flg, cnt);

    }

    public int dfs(int m, int n, int i, int j, boolean[][] flg, int cnt) {
        // 判断越界以及是否遍历过以及是否符合整理要求
        if (i < 0 || i >= m || j < 0 || j >= n || flg[i][j] || (numSum(i) + numSum(j) > cnt)) {
            return 0;
        }
        // 标记一下
        flg[i][j] = true;
        return dfs(m, n, i, j + 1, flg, cnt) + dfs(m, n, i + 1, j, flg, cnt) + 1;
    }


    public int numSum(int num) {
        int sum = 0;
        while (num != 0) {
            sum += num % 10;
            num /= 10;
        }
        return sum;
    }
}

72. LCR 146. 螺旋遍历二维数组

代码实现:

class Solution {
    public int[] spiralArray(int[][] array) {
        if (array.length == 0)
            return new int[0];
        int x1 = 0, y1 = 0;
        int x2 = array.length - 1, y2 = array[0].length - 1;
        int k = 0;
        int[] ret = new int[(x2 + 1) * (y2 + 1)];
        while (x1 <= x2 && y1 <= y2) {
            // 第一行
            for (int i = y1; i <= y2; i++) {
                ret[k++] = array[x1][i];
            }
            // 最后一列
            for (int i = x1 + 1; i <= x2; i++) {
                ret[k++] = array[i][y2];
            }
            // 最后一行
            if (x1 < x2) {
                for (int i = y2 - 1; i >= y1; i--) {
                    ret[k++] = array[x2][i];
                }
            }
            // 第一列
            if (y1 < y2) {
                for (int i = x2 - 1; i > x1; i--) {
                    ret[k++] = array[i][y1];
                }
            }

            x1++;
            y1++;
            x2--;
            y2--;

        }

        return ret;

    }
}

贪心算法

73. LCR 188. 买卖芯片的最佳时机

让你求最大值,第一眼就是两层遍历搞定。

但是还有另一种方法,要想获得利润最大值,一般是低买高卖,低价买入高价卖出,所以我们可以定义一个变量 minPrice 记录股票的最小值,再定义一个 max 来记录最大利润,只要当前股票 - minPrice 比 max 大,max 就更新。最后返回 max 即可。

代码实现:

class Solution {
    public int bestTiming(int[] prices) {
        int max = 0;
        for (int i = 0; i < prices.length - 1; i++) {
            for (int j = i + 1; j < prices.length; j++) {
                int tmp = prices[j] - prices[i];
                if (tmp > max) {
                    max = tmp;
                }
            }
        }
        return max;
    }
}



class Solution {
    public int bestTiming(int[] prices) {
        int max = 0;
        // 记录最低股票值
        int minPrice = Integer.MAX_VALUE;
        // 低买高卖
        for (int i = 0; i < prices.length; i++) {
            if (prices[i] < minPrice) {
                minPrice = prices[i];
            }
            if (prices[i] - minPrice > max) {
                max = prices[i] - minPrice;
            }
        }
        return max;
    }
}

找规律

74. LCR 162. 数字 1 的个数

可以去看看大佬的题解。

代码实现:

class Solution {
    public int digitOneInNumber(int num) {
        int digit = 1, count = 0;
        int high = num / 10, cur = num % 10, low = 0;
        while (high != 0 || cur != 0) {// 当 cur 和高位都是 0,则说明已经遍历完该数字了
            if (cur == 0) {
                // 当前位是 0
                count += high * digit;
            } else if (cur == 1) {
                // 当前位是 1
                count += high * digit + low + 1;
            } else {
                count += (high + 1) * digit;
            }
            // 1   2   3   4
            //  high  cur low
            low += cur * digit;
            cur = high % 10;// 为 high 的最后一位
            high /= 10;
            digit *= 10;
        }
        return count;
    }
}

75. LCR 163. 找到第 k 位数字

可以去看看大佬题解。

代码实现:

class Solution {
    public int findKthNumber(int k) {
        // 几位数
        int digit = 1;
        // 从哪里开始
        long start = 1;
        // digit 位数一共有几个
        long count = 9;
        // 获取第 k 位数 是几位数
        while (k > count) {
            k -= count;
            digit++;
            start *= 10;
            count = 9 * start * digit;
        }
        // 确定 k 所在的数字
        // start +  (k - 1) / digit 用来计算从 start 开始跳过多少个完整的数字
        long num = start + (k - 1) / digit;
        return Long.toString(num).charAt((k - 1) % digit) - '0';
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值