Skip to content

Latest commit

 

History

History
458 lines (315 loc) · 23.1 KB

File metadata and controls

458 lines (315 loc) · 23.1 KB

没想到“每日 1 题”又延长了一个月。每日 1 题:6 月(部分)。

1 ✅ 2 ✅ 3 ✅ 4 ✅ 5 ✅ 6 ✅ 7 ✅
8 ✅ 9 ✅ 10 ✅ 11 ✅ 12 ✅ 13 ✅ 14 ✅
15 ✅ 16 ✅ 17 ✅ 18 ✅ 19 ✅ 20 ✅ 21 ✅
22 ✅ 23 ✅ 24 ✅ 25 ✅ 26 ✅ 27 ✅ 28 ✅
29 ✅ 30 ✅ 1 2 3 4 5

打卡完毕 🎉。不知不觉间“每日 1 题”已经持续四个月了,其中的 122 题覆盖了大部分题型,从中学到了很多,但因为“战线”拉得太长,很多解题思想都遗忘了,需要时不时进行复习。

四月底的时候确定了实习地点,之后做“每日 1 题”的动力就有点不足,但出于强迫症不想“断签”,也为以后可以更方便地复习,因此做了一个简单的记录。“每日 1 题” 7 月仍在继续,但接下来就要期末考和实习了,不准备再继续了。


1. 求 1+2+…+n

!> 面试题64. 求1+2+…+n

剑指 Offer 中的题,之前做过。题目要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句(A?B:C)。

这是很有意思的一道题目。若无上述限制,要完成这道题可以有三种做法:等差数列求和,迭代和递归。然而题目规定不能使用乘除和循环语句,因此剩下递归可用,但题目又规定不能使用条件判断语句,那么如何判停呢?

利用逻辑运算符的短路效应

  • “与 &&”。if(A && B),若 A 为 false ,则 B 的判断不会执行,直接判定 A && B 为 false
  • “或 ||”。if(A || B),若 A 为 true ,则 B 的判断不会执行,直接判定 A || B 为 true

我们需要在 n == 1 时终止递归,那判停语句为:boolean b = (n > 1) && ((sum = n + sumNums(n - 1)) > 0),为构成语句,需要添加一个 boolean b;由于 && 右边本身为 int 所以需要一个加一个判断条件变为 boolean

public int sumNums(int n) {
    int sum = n;
    boolean b = (n > 1) && ((sum = n + sumNums(n - 1)) > 0);
    return sum;
}

评论区看到另一种递归做法:

int[] test = new int[]{0};
public int sumNums(int n) {
    try {
        return test[n];
    } catch(Exception e) {
        return n + sumNums(n - 1);
    }
}

官方题解中还有一种“快速乘”的解法。

2. ⭐ 新 21 点

!> 837. 新21点

想不到思路 0.0。题解给出动规方法,以下摘自题解

爱丽丝获胜的概率只和下一轮开始前的得分有关,因此令 $dp[x]$ 表示从得分为 $x$ 的情况开始游戏并且获胜的概率,目标是求 $dp[0]$ 的值。

根据规则,当分数达到或超过 $K$ 时游戏结束,此时:

  • 若分数不超过 $N$ 则获胜。有 $dp[x] = 1 \quad K \leq x \leq \min (N, K+W-1)$。若 $K - 1$ 时抽到的数为 $W$,且 $K - 1 + W < N$,因此有 $min (N, K+W-1)$
  • 若分数超过 $N$ 则失败。有 $dp[x] = 0 \quad x>\min (N, K+W-1)$

$0 \leq x < K$ 时,如何计算 $dp[x]$ 的值?注意到每次在范围 $[1, W]$ 内随机抽取一个整数,且每个整数被抽取到的概率相等,因此可以得到如下状态转移方程:

$$dp[x]=\frac{dp[x+1]+dp[x+2]+\cdots+dp[x+W]}{W}$$

上述解法在 $0 \leq x < K$ 时,比较费时,需要优化。考虑对 $dp$ 的相邻项计算差分并移项(偏数学了),有如下结果:

$$dp[x]=dp[x+1]-\frac{dp[x+W+1]-dp[x+1]}{W}$$

其中 $0 \leq x < K - 1$,我们需要对 $dp[K - 1]$ 单独计算:

$$dp[K-1]=\frac{dp[K]+dp[K+1]+\cdots+dp[K+W-1]}{W}$$

注意到只有当 $K \leq x\leq \min(N, K+W-1)$ 时才有 $dp[x]=1$,因此

$$dp[K-1]=\frac{\min (N, K+W-1)-K+1}{W}=\frac{\min (N-K+1, W)}{W}$$

可在 $O(1)$ 内计算出 $dp[K - 1]$。对于 $dp[K-2]$$dp[0]$ 的值,则可通过新的状态转移方程得到。

代码

3. 除自身以外数组的乘积

!> 238. 除自身以外数组的乘积

题目要求不能使用除法,且在 $O(n)$ 时间复杂度内完成此题。

每个位置的结果,为 $它左边的数的乘积 * 它右边的数的乘积$。那么我们用两个数组来保存每个位置左边所有数的乘积和右边所有数的乘积即可。

进阶:你可以在常数空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)

由于输出数组不被视为额外空间,我们可以暂时用它来存储左边的数的乘积,对于右边数的乘积 $R$,我们可以用一个常数来记录,在倒序遍历 nums[] 时,更新 $R$ 和输出数组。

public static int[] productExceptSelf(int[] nums) {
    int n = nums.length;
    int[] product = new int[n];
    
    product[0] = 1;
    for (int i = 1; i < n; i++) {
        product[i] = product[i - 1] * nums[i - 1];
    }
    
    int R = 1;
    for (int i = n - 1; i >= 0; i--) {
        product[i] = product[i] * R;
        R *= nums[i];
    }
    return product;
}

4. 顺时针打印矩阵

!> 面试题29. 顺时针打印矩阵

模拟

定义一个“方向”数组 int[][] direction = new int[][]{{0, 1}, {1, 0}, {0, -1}, {-1, 0}} 来控制遍历方向,当下一个位置超出边界或者已访问(设一个 boolean[][] hasVisited 数组)时,就改变方向。

定义一个 i 来记录已遍历的元素数量,并根据总的元素数量来判断是否结束遍历。

按层模拟

参考自题解

可以将矩阵看成若干层,首先打印最外层的元素,其次打印次外层的元素,直到打印最内层的元素。下图矩阵最外层元素都是第 1 层,次外层元素都是第 2 层,剩下的元素都是第 3 层。

[[1, 1, 1, 1, 1, 1, 1],
 [1, 2, 2, 2, 2, 2, 1],
 [1, 2, 3, 3, 3, 2, 1],
 [1, 2, 2, 2, 2, 2, 1],
 [1, 1, 1, 1, 1, 1, 1]]

按下图遍历每一层:

遍历完当前层的元素之后,将 left 和 top 分别增加 1,将 right 和 bottom 分别减少 1,进入下一层继续遍历,直到遍历完所有元素为止,终止条件为 left > right || top > bottom

5. ⭐ 最长连续序列

!> 128. 最长连续序列(困难)

给定一个未排序的整数数组,找出最长连续序列的长度。要求算法的时间复杂度为 $O(n)$

用哈希表存储每个端点值对应连续区间的长度,摘自力扣评论区

  • 若数 num 已在哈希表中,则跳过
  • 若不在哈希表中,
    • 取出其左右相邻数已有的连续区间长度 leftLenrightLen
    • 计算当前数的区间长度为:curLen = leftLen + rightLen + 1
    • 根据 curLen 更新最大长度 maxLen 的值
    • 更新区间两端点的长度值,即更新 num - leftLennum + rightLen 的在哈希表中的值

6. 单词接龙 II

!> 126. 单词接龙 II(困难)

相似题目“127. 单词接龙”,都可以用 BFS 解决

beginWord 开始,寻找 wordList 中所有符合条件的下一个单词并加入 BFS 队列。值得注意的是,BFS 队列里存储的元素应该是一条“路径”(List<String>),它存储从 beginWord 到该层该元素的路径。一旦某层有元素到达了 endWord,说明该层为最短路径,我们只需在该层所有输出能到达 endWord 的路径,并结束 BFS。

在 BFS 的同时,我们要维护一个 Visited 集合,记录已被加入路径的单词。但是,在把单词添加到某路径的时候,不能直接将该单词在 Visited 中标记为已访问,因为该层的其他路径也有可能会加入这个单词。所以,应该存储一个临时的 subVisited,在该层结束之后,再汇合到总的已访问单词集合中。

思路参考:https://leetcode-cn.com/problems/word-ladder-ii/solution/java-duo-jie-fa-bfs-shuang-xiang-bfsdfssi-lu-fen-x/

代码更容易懂。

7. 等式方程的可满足性

!> 990. 等式方程的可满足性

考查并查集知识

由于等式方程具有传递性,比如 a == bb == c 成立,则 a == c,可以想到用并查集来维护这种关系。

我们可以遍历 equations 数组中的等式,我们把等式两边的变量合并。接着再遍历数组中的不等式,不等式中的两个变量不能属于同一个集合,若查找后是同一个集合,则出现矛盾。

并查集的合并操作:

private static void union(int x, int y, int[] father) {
    int faX = findFather(x, father);
    int faY = findFather(y, father);
    if (faX != faY) {
        father[faX] = faY;
    }
}

并查集的查找操作:

private static int findFather(int x, int[] father) {
    while (x != father[x]) {
        x = father[x];
    }
    return x;
}

路径压缩后的查找:

private static int findFather(int x, int[] father) {
    // 由于 x 在下面的 while 中会变成根结点,因此先把原先的 x 保存一下
    int oriX = x;
    while (x != father[x]) {
        x = father[x];
    }
    // 下面把路径上的所有结点的 father 都改成根结点
    while (oriX != father[oriX]) {
        int temp = oriX;
        oriX = father[oriX];
        father[temp] = x;
    }
    return x;
}

8. 把数字翻译成字符串

!> 面试题46. 把数字翻译成字符串

DFS 或动态规划都可以。本题和“91. 解码方法”相似,略有不同的是, 91 题的 $1$ 代表 $A$,而本题的 $0$ 代表 $a$

DFS

DFS 的思路很直观:每次有两种选择,选择一位数字翻译或两位数字翻译,具体情况如代码所示。

if (headNum == '0' || headNum > '2') {
    DFS(str.substring(1, len));
} else {
    DFS(str.substring(1, len));
    if (str.length() > 1) {
        if (!(headNum == '2' && str.charAt(1) > '5')) {
            DFS(str.substring(2, len));
        }
    }
}

无备忘录的 DFS 会存在重复的计算,时间复杂度并不理想。如下图:

图片来自力扣

动态规划

若当前数字和前一个数字可以合并翻译(说明既可以单独翻译,又可以合并翻译),那么:$dp[i] = dp[i - 1] + dp[i - 2]$;否则,$dp[i] = dp[i - 1]$。

int[] dp = new int[len + 1];
dp[0] = dp[1] = 1;
for (int i = 2; i <= len; i++) {
    char preChar = str.charAt(i - 2);
    char curChar = str.charAt(i - 1);
    if (preChar == '1' || (preChar == '2' && curChar >= '0' && curChar <= '5')) {
        dp[i] = dp[i - 1] + dp[i - 2];
    } else {
        dp[i] = dp[i - 1];
    }
}

91. 解码方法”情况更复杂,👉 思路

9. 每日温度

!> 739. 每日温度

倒序遍历数组,并维护一个栈。若栈顶温度小于当前温度,则弹出;否则,记录栈顶温度和当前温度相差的天数,并存入结果数组。最后将当前温度入栈。

public static int[] dailyTemperatures(int[] T) {
    int len = T.length;
    int[] res = new int[len];
    LinkedList<Integer> stack = new LinkedList<>();
    for (int i = len - 1; i >= 0; i--) {
        int curTemperature = T[i];
        int daysToWait = 0;
        while (!stack.isEmpty()) {
            if (T[stack.getLast()] > curTemperature) {
                daysToWait = stack.getLast() - i;
                break;
            }
            stack.pollLast();
        }
        res[i] = daysToWait;
        stack.addLast(i);
    }
    return res;
}

10. 三数之和

!> 15. 三数之和

之前做过,👉 传送门

11. 二叉树的序列化与反序列化

!> 297. 二叉树的序列化与反序列化(困难)

序列化方式有多种,可以是层序、先序、中序或后序。只要反序列化和序列化对应上就可以。

12. 最佳观光组合

!> 1014. 最佳观光组合

由题意可知,求一对景点的最高得分就是求 A[i] + A[j] + i - j 的最大值,我们把它拆分为 A[i] + iA[j] - j 两部分。首先计算出 A[i] + i,然后倒序遍历计算 A[j] - j 并维护当前最大的 A[j] - j 记为 curMaxJ。由于 A[i] + i 的值是固定的,在倒序时,我们只需计算当前的 curMaxJA[i] + i 之和,并更新这个和的最大值即可。

public static int maxScoreSightseeingPair(int[] A) {
    int len = A.length;
    int[] aPlusI = new int[len];
    int maxScore = 0;

    for (int i = 0; i < len; i++) {
        aPlusI[i] = A[i] + i;
    }
    int curMaxJ = A[len - 1] - (len - 1);
    for (int j = len - 1; j > 0; j--) {
        curMaxJ = Math.max(curMaxJ, A[j] - j);
        maxScore = Math.max(maxScore, aPlusI[j - 1] + curMaxJ);
    }
    
    return maxScore;
}

当然可以取消计算 A[i] + i 的那个循环,直接放到第二个循环计算,不过分开循环计算思路更清晰。

13. 从先序遍历还原二叉树

!> 1028. 从先序遍历还原二叉树(困难)

占位

14. 正则表达式匹配

!> 10. 正则表达式匹配(困难)

可用 DFS 和动态规划思想。DFS 在这里,动态规划为官方题解

15. 二叉树中的最大路径和

!> 124. 二叉树中的最大路径和(困难)

这题和“543. 二叉树的直径”思路相同。

DFS

我们可以知道,最大路径和一定是 某一结点的值 + 此结点左子树的最长路径 + 此结点右子树的最长路径。我们计算所有结点的 结点的值 + 此结点左子树的最长路径 + 此结点右子树的最长路径 的值,并记录最大值,即为最大路径和。

对于 结点左\右子树的最长路径,我们可以通过 DFS 返回。代码思路更清晰。

private static int maxPath;
public static int maxPathSum(TreeNode root) {
    maxPath = Integer.MIN_VALUE;
    DFS(root);
    return maxPath;
}
private static int DFS(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int leftHeight = Math.max(0, DFS(root.left));
    int rightHeight = Math.max(0, DFS(root.right));
    maxPath = Math.max(maxPath, leftHeight + rightHeight + root.val);
    return root.val + Math.max(leftHeight, rightHeight);
}

16. 💣 模式匹配

!> 面试题 16.18. 模式匹配

要使 pattern 匹配 value,需要 pattern 中的 ab 分别代表不同的字符串,且 countA * lenA + countB * lenB = value.length()countAcountBvalue.length() 已知,那么我们可以通过解一元二次方程来枚举 ab 的长度,再进行匹配。

但本题允许 ab 为空串,因此边界条件需要仔细考虑,看题解

17. 最接近的三数之和

!> 16. 最接近的三数之和

排序 + 双指针。和“15. 三数之和”思路相同 👉 在这里

18. 单词拆分

!> 139. 单词拆分

因为字典中的单词可以被重复使用,所以这是一个完全背包问题,在动态规划分类中做过 👉 传送门

19. 缺失的第一个正数

!> 41. 缺失的第一个正数(困难)

[!NOTE|label:要求] 你的算法的时间复杂度应为 $O(n)$,并且只能使用常数级别的额外空间

由于有空间复杂度的要求,我们不能用哈希表 + 枚举正整数(从 $1$ 开始,若不在哈希表中,就是答案)的方法来实现。想不到方法,直接看了题解,思路如下:

原地哈希

我们对数组进行遍历,对于遍历到的数 $x$,如果它在 $[1, N]$ 的范围内,那么就将数组中的第 $x - 1$ 个位置(注意:数组下标从 $0$ 开始)打上“标记”。在遍历结束之后,如果所有的位置都被打上了标记,那么答案是 $N + 1$,否则答案是最小的没有打上标记的位置加 1。

20. 长度最小的子数组

!> 209. 长度最小的子数组

因为给定的是正整数的数组,所以双指针完事,sum >= s 则左指针右移。

public static int minSubArrayLen(int s, int[] nums) {
    if (nums.length == 0) {
        return 0;
    }
    int l = 0, r = 0;
    int sum = 0, len = Integer.MAX_VALUE;
    while (r < nums.length) {
        sum += nums[r];
        while (sum >= s) {
            len = Math.min(len, r - l + 1);
            sum -= nums[l];
            l++;
        }
        r++;
    }
    return len == Integer.MAX_VALUE ? 0 : len;
}

此外,还有

  • 暴力法:双层循环
  • 前缀和 + 二分查找:sums 数组保存前缀和,通过二分查找得到大于或等于 i 的最小下标 bound,使得 sums[bound]−sums[i−1] >= s

21. 数组中的第 K 个最大元素

!> 215. 数组中的第K个最大元素

三种方法:排序、快速选择、堆选择。在排序章节做过 👉 这里