LeetCode 726-739. 题解合集
726. Number of Atoms(原子的数量)
题目描述
给你一个字符串化学式 formula,返回每种原子的数量。
原子总是以一个大写字母开始,接着跟随 0 个或任意个小写字母,表示原子的名字。
如果数量大于 1,原子后会跟着数字表示原子的数量。如果数量等于 1 则不会跟数字。
两个化学式连在一起可以构成新的化学式;由括号括起的化学式并佐以数字(可选择性添加)
也是化学式。
返回所有原子的数量,格式为:第一个(按字典序)原子的名字,跟着它的数量(如果数量大于
1),然后是第二个原子的名字(按字典序),跟着它的数量(如果数量大于 1),以此类推。
示例 1
- 输入:formula = "H2O"
- 输出:"H2O"
- 解释:原子的数量是 {'H': 2, 'O': 1}。
示例 2
- 输入:formula = "Mg(OH)2"
- 输出:"H2MgO2"
- 解释:原子的数量是 {'H': 2, 'Mg': 1, 'O': 2}。
示例 3
- 输入:formula = "K4(ON(SO3)2)2"
- 输出:"K4N2O14S4"
- 解释:原子的数量是 {'K': 4, 'N': 2, 'O': 14, 'S': 4}。
提示: 1 <= formula.length <= 1000formula由英文字母、数字、'('和')'组成formula总是有效的化学式
解题思路
解法一(栈 + TreeMap,主提交):顺序扫描化学式:
- 大写字母后跟小写字母组成原子名;
- 原子后跟数字为倍数;
- 遇到
(压入一个空计数表,遇到)弹出并把括号内计数乘以后面的数字合并到上一层。
计数表统一用TreeMap<String, Integer>保持字典序,最后拼装结果串。
时间复杂度 O(n·log k),空间复杂度 O(n)。
解法二(递归下降解析):把(视为递归子程序,返回该段原子计数表;读到)结束递归。
思路与栈等价,便于调试。
代码实现
import java.util.Map;
import java.util.TreeMap;
class Solution {
// 解法一:栈 + TreeMap
public String countOfAtoms(String formula) {
// 用计数数组表模拟栈:每层一个 TreeMap
java.util.Deque<TreeMap<String, Integer>> stack = new java.util.ArrayDeque<>();
TreeMap<String, Integer> cur = new TreeMap<>();
int n = formula.length();
int i = 0;
while (i < n) {
char c = formula.charAt(i);
if (c == '(') {
stack.push(cur);
cur = new TreeMap<>();
++i;
} else if (c == ')') {
++i;
int num = 0;
while (i < n && Character.isDigit(formula.charAt(i))) {
num = num * 10 + (formula.charAt(i) - '0');
++i;
}
num = num == 0 ? 1 : num;
TreeMap<String, Integer> top = stack.pop();
for (Map.Entry<String, Integer> e : cur.entrySet()) {
top.merge(e.getKey(), e.getValue() * num, Integer::sum);
}
cur = top;
} else if (Character.isUpperCase(c)) {
int j = i + 1;
while (j < n && Character.isLowerCase(formula.charAt(j))) {
++j;
}
String name = formula.substring(i, j);
i = j;
int num = 0;
while (i < n && Character.isDigit(formula.charAt(i))) {
num = num * 10 + (formula.charAt(i) - '0');
++i;
}
num = num == 0 ? 1 : num;
cur.merge(name, num, Integer::sum);
} else {
++i; // 防御:不会出现
}
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Integer> e : cur.entrySet()) {
sb.append(e.getKey());
if (e.getValue() > 1) {
sb.append(e.getValue());
}
}
return sb.toString();
}
// 解法二:递归解析(下标引用封装)
private int pos;
private String f;
public String countOfAtoms2(String formula) {
pos = 0;
f = formula;
TreeMap<String, Integer> res = parseExpr();
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Integer> e : res.entrySet()) {
sb.append(e.getKey());
if (e.getValue() > 1) {
sb.append(e.getValue());
}
}
return sb.toString();
}
private TreeMap<String, Integer> parseExpr() {
TreeMap<String, Integer> map = new TreeMap<>();
while (pos < f.length() && f.charAt(pos) != ')') {
char c = f.charAt(pos);
if (c == '(') {
++pos; // 跳过 '('
TreeMap<String, Integer> inner = parseExpr();
++pos; // 跳过 ')'
int num = readNum();
for (Map.Entry<String, Integer> e : inner.entrySet()) {
map.merge(e.getKey(), e.getValue() * num, Integer::sum);
}
} else {
int j = pos + 1;
while (j < f.length() && Character.isLowerCase(f.charAt(j))) {
++j;
}
String name = f.substring(pos, j);
pos = j;
int num = readNum();
map.merge(name, num, Integer::sum);
}
}
return map;
}
private int readNum() {
int num = 0;
while (pos < f.length() && Character.isDigit(f.charAt(pos))) {
num = num * 10 + (f.charAt(pos) - '0');
++pos;
}
return num == 0 ? 1 : num;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.countOfAtoms("H2O").equals("H2O")) ++pass; else System.out.println("FAIL test1: " + sol.countOfAtoms("H2O"));
++total; if (sol.countOfAtoms("Mg(OH)2").equals("H2MgO2")) ++pass; else System.out.println("FAIL test2: " + sol.countOfAtoms("Mg(OH)2"));
++total; if (sol.countOfAtoms("K4(ON(SO3)2)2").equals("K4N2O14S4")) ++pass; else System.out.println("FAIL test3: " + sol.countOfAtoms("K4(ON(SO3)2)2"));
++total; if (sol.countOfAtoms("Be32").equals("Be32")) ++pass; else System.out.println("FAIL test4: " + sol.countOfAtoms("Be32"));
++total; if (sol.countOfAtoms2("H2O").equals("H2O")) ++pass; else System.out.println("FAIL test5");
++total; if (sol.countOfAtoms2("Mg(OH)2").equals("H2MgO2")) ++pass; else System.out.println("FAIL test6");
++total; if (sol.countOfAtoms2("K4(ON(SO3)2)2").equals("K4N2O14S4")) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0726 PASS ==");
} else {
System.out.println("== 0726 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n·logk) | O(n) |
| 解法二 | O(n·logk) | O(n) |
727. Minimum Window Subsequence(最小窗口子序列)
题目描述
给定字符串 s1 和 s2,找出 s1 中最短的连续子串,使得 s2 是该子串的子序列。
如果 s1 中没有窗口可以包含 s2 中的所有字符,返回空字符串 ""。如果有不止一个最短
长度的窗口,返回开始位置最靠左的那个。
示例 1
- 输入:s1 = "abcdebdde", s2 = "bde"
- 输出:"bcde"
- 解释:"bcde" 是答案,因为它在相同长度的字符串 "bdde" 出现之前。"deb" 不是一个更短的答案,
因为在窗口中必须按顺序出现 s2 中的元素。
示例 2
- 输入:s1 = "jmeqksfrsdcmsiwvaovztaqenprpvnbstl", s2 = "u"
- 输出:""
提示: 1 <= s1.length <= 2 * 10^41 <= s2.length <= 100s1和s2只包含小写英文字母
解题思路
解法一(DP 记录子序列起点,主提交):dp[i][j] 表示在 s1[0..i](含 i)中匹配
s2[0..j] 时,这个子序列在 s1 中尽量靠右的起点位置;无法匹配则为 -1。
转移:若 s1[i] == s2[j],则 dp[i][j] = dp[i-1][j-1](用上 s1[i] 作第 j 个字符);
否则 dp[i][j] = dp[i-1][j](不用 s1[i])。列 j = n-1 的每个 i 处,
i - dp[i][n-1] + 1 就是覆盖 s2 的窗口长度。取最小长度、最靠左起点。
时间复杂度 O(m·n),空间复杂度 O(m·n)(可滚动到 O(n))。
解法二(双指针前向匹配 + 回退收缩):贪心从左到右:前向匹配完 s2,再回退找到最左起点,
记录窗口;随后从起点+1 继续,允许窗口重叠。时间复杂度 O(m·n)(最坏),空间 O(1)。
代码实现
class Solution {
// 解法一:DP
public String minWindow(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m][n];
for (int i = 0; i < m; ++i) {
dp[i][0] = (s1.charAt(i) == s2.charAt(0)) ? i : (i > 0 ? dp[i - 1][0] : -1);
}
for (int j = 1; j < n; ++j) {
for (int i = 0; i < m; ++i) {
if (s1.charAt(i) == s2.charAt(j)) {
dp[i][j] = (i > 0 && dp[i - 1][j - 1] >= 0) ? dp[i - 1][j - 1] : -1;
} else {
dp[i][j] = (i > 0) ? dp[i - 1][j] : -1;
}
}
}
int bestLen = Integer.MAX_VALUE;
int bestStart = -1;
for (int i = 0; i < m; ++i) {
if (dp[i][n - 1] >= 0) {
int len = i - dp[i][n - 1] + 1;
if (len < bestLen) {
bestLen = len;
bestStart = dp[i][n - 1];
}
}
}
return bestStart == -1 ? "" : s1.substring(bestStart, bestStart + bestLen);
}
// 解法二:双指针 + 回退收缩
public String minWindow2(String s1, String s2) {
int m = s1.length(), n = s2.length();
int ansStart = -1, minLen = Integer.MAX_VALUE;
int i = 0;
while (i < m) {
int j = 0;
while (i < m && j < n) {
if (s1.charAt(i) == s2.charAt(j)) {
++j;
}
++i;
}
if (j < n) {
break; // 其余 s1 也无法匹配完 s2
}
// i 指向 s2 全部匹配后的下一个位置,end 指向最后一个匹配字符
int end = i - 1;
int k = n - 1;
while (k >= 0) {
if (s1.charAt(end) == s2.charAt(k)) {
--k;
}
--end;
}
++end; // 窗口左起点
int len = (i - 1) - end + 1;
if (len < minLen) {
minLen = len;
ansStart = end;
}
i = end + 1; // 允许重叠继续找
}
return ansStart == -1 ? "" : s1.substring(ansStart, ansStart + minLen);
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.minWindow("abcdebdde", "bde").equals("bcde")) ++pass; else System.out.println("FAIL test1: " + sol.minWindow("abcdebdde", "bde"));
++total; if (sol.minWindow("jmeqksfrsdcmsiwvaovztaqenprpvnbstl", "u").equals("")) ++pass; else System.out.println("FAIL test2");
++total; if (sol.minWindow("abc", "abc").equals("abc")) ++pass; else System.out.println("FAIL test3: " + sol.minWindow("abc", "abc"));
++total; if (sol.minWindow("abbc", "bc").equals("bc")) ++pass; else System.out.println("FAIL test4: " + sol.minWindow("abbc", "bc"));
++total; if (sol.minWindow2("abcdebdde", "bde").equals("bcde")) ++pass; else System.out.println("FAIL test5: " + sol.minWindow2("abcdebdde", "bde"));
++total; if (sol.minWindow2("jmeqksfrsdcmsiwvaovztaqenprpvnbstl", "u").equals("")) ++pass; else System.out.println("FAIL test6");
++total; if (sol.minWindow2("abc", "abc").equals("abc")) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0727 PASS ==");
} else {
System.out.println("== 0727 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m·n) | O(m·n) |
| 解法二 | O(m·n) | O(1) |
728. Self Dividing Numbers(自除数)
题目描述
自除数是指可以被它包含的每一位数整除的数。
- 例如,
128是一个自除数,因为128 % 1 == 0,128 % 2 == 0,128 % 8 == 0。
还有,自除数不允许包含 0。
给定上边界left和right,输出一个列表,列表的元素是范围[left, right]内所有的自除数。
示例 1
- 输入:left = 1, right = 22
- 输出:[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 15, 22]
示例 2
- 输入:left = 47, right = 85
- 输出:[48,55,66,77]
提示: 1 <= left <= right <= 10^4
解题思路
解法一(逐数判断,主提交):对 [left, right] 中每个数,不断取末位并剔除,检查每一位
非 0 且能整除原数。时间复杂度 O((right-left) · digit),空间复杂度 O(1)。
解法二(预计算 1~10^4 的自除数集合):直接打表或预筛选后判成员。适合多次查询场景。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
private boolean isSelfDividing(int x) {
int t = x;
while (t > 0) {
int d = t % 10;
if (d == 0 || x % d != 0) {
return false;
}
t /= 10;
}
return true;
}
// 解法一:逐数判断
public List<Integer> selfDividingNumbers(int left, int right) {
List<Integer> ans = new ArrayList<>();
for (int x = left; x <= right; ++x) {
if (isSelfDividing(x)) {
ans.add(x);
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.selfDividingNumbers(1, 22).equals(java.util.Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 15, 22))) ++pass; else System.out.println("FAIL test1: " + sol.selfDividingNumbers(1, 22));
++total; if (sol.selfDividingNumbers(47, 85).equals(java.util.Arrays.asList(48, 55, 66, 77))) ++pass; else System.out.println("FAIL test2: " + sol.selfDividingNumbers(47, 85));
if (pass == total) {
System.out.println("== 0728 PASS ==");
} else {
System.out.println("== 0728 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O((R-L)·logR) | O(1) |
| 解法二 | O(10^4)(预计算) | O(10^4) |
729. My Calendar I(我的日程安排表 I)
题目描述
实现一个 MyCalendar 类来存放你的日程安排。如果要添加的日程安排不会造成重复预订,则
可以存储这个新的日程安排。
当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订。
日程可以用一对整数 startTime 和 endTime 表示,这里的时间是半开区间,即
[startTime, endTime)。
实现 MyCalendar 类:
MyCalendar()初始化日历对象。boolean book(int startTime, int endTime)如果可以将日程安排成功添加到日历中而不会导致
重复预订,返回true。否则,返回false并且不要将该日程安排添加到日历中。
示例
- 输入:
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]] - 输出:
[null, true, false, true] - 解释:
MyCalendar myCalendar = new MyCalendar();
myCalendar.book(10, 20); // 返回 True
myCalendar.book(15, 25); // 返回 False ,这个日程安排不能添加到日历中,因为时间 15 已经被
// 之前的日程安排预定(区间 [15, 20) 有重叠)
myCalendar.book(20, 30); // 返回 True
提示: 0 <= start < end <= 10^9- 每个测试用例,调用
book方法最多不超过10^4次
解题思路
解法一(TreeMap 维护区间,主提交):用 TreeMap<Integer, Integer> 存 起点 -> 终点。
book 时找 floorKey(start)(其终点 > start 则冲突)和 ceilingKey(start)(其起点 < end
则冲突)。不冲突则插入。时间复杂度 O(log n),空间复杂度 O(n)。
解法二(朴素列表扫描):直接把区间存 List<int[]>,每次 book 线性检查冲突。
时间复杂度 O(n),空间复杂度 O(n)。
代码实现
import java.util.Map;
import java.util.TreeMap;
class MyCalendar {
private final TreeMap<Integer, Integer> map = new TreeMap<>();
public MyCalendar() {
}
public boolean book(int startTime, int endTime) {
Map.Entry<Integer, Integer> lo = map.floorEntry(startTime);
if (lo != null && lo.getValue() > startTime) {
return false;
}
Map.Entry<Integer, Integer> hi = map.ceilingEntry(startTime);
if (hi != null && hi.getKey() < endTime) {
return false;
}
map.put(startTime, endTime);
return true;
}
// ===== 本地测试 =====
public static void main(String[] args) {
int pass = 0, total = 0;
MyCalendar mc = new MyCalendar();
++total; if (mc.book(10, 20)) ++pass; else System.out.println("FAIL test1");
++total; if (!mc.book(15, 25)) ++pass; else System.out.println("FAIL test2");
++total; if (mc.book(20, 30)) ++pass; else System.out.println("FAIL test3");
++total; if (mc.book(5, 10)) ++pass; else System.out.println("FAIL test4");
++total; if (!mc.book(5, 8)) ++pass; else System.out.println("FAIL test5");
if (pass == total) {
System.out.println("== 0729 PASS ==");
} else {
System.out.println("== 0729 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(log n) | O(n) |
| 解法二 | O(n) | O(n) |
730. Count Different Palindromic Subsequences(统计不同回文子序列)
题目描述
给你一个字符串 s,返回 s 中不同的非空回文子序列个数。由于答案可能很大,请返回对
10^9 + 7 取余的结果。
字符串的子序列可以经由字符串删除 0 个或多个字符获得。
如果一个序列与它反转后的序列一致,那么它是回文序列。
如果存在某个 i,满足 a_i != b_i,则两个序列 a1, a2, ... 和 b1, b2, ... 不同。
示例 1
- 输入:s = 'bccb'
- 输出:6
- 解释:6 个不同的非空回文子字符序列分别为:'b', 'c', 'bb', 'cc', 'bcb', 'bccb'。
注意:'bcb' 虽然出现两次但仅计数一次。
示例 2
- 输入:s = 'abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba'
- 输出:104860361
提示: 1 <= s.length <= 1000s[i]仅包含'a'、'b'、'c'或'd'
解题思路
解法一(区间 DP,主提交):dp[i][j] 表示 s[i..j] 内不同非空回文子序列个数。
关键转移(分为 s[i] != s[j] 与 s[i] == s[j] 两类):
- 若
s[i] != s[j]:dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1](容斥); - 若
s[i] == s[j]:设l = i+1找第一个等于s[i]的位置,r = j-1找最后一个。
记k = s[i] - 'a': - 不存在重复字符:
dp[i][j] = 2*dp[i+1][j-1] + 2(外层两个字符成对 + 单字符); - 恰一个重复字符:
dp[i][j] = 2*dp[i+1][j-1] + 1; - 存在多个重复:
dp[i][j] = 2*dp[i+1][j-1] - dp[l+1][r-1](减去重复计数)。
全程取模。时间复杂度 O(n²),空间复杂度 O(n²)。
解法二(记忆化搜索):与区间 DP 相同公式,用递归 + memo 实现,避免按长度枚举。
代码实现
class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:区间 DP
public int countPalindromicSubsequences(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int i = 0; i < n; ++i) {
dp[i][i] = 1;
}
for (int len = 2; len <= n; ++len) {
for (int i = 0; i + len - 1 < n; ++i) {
int j = i + len - 1;
if (s.charAt(i) != s.charAt(j)) {
dp[i][j] = (dp[i + 1][j] + dp[i][j - 1]) % MOD;
dp[i][j] = (dp[i][j] - dp[i + 1][j - 1] + MOD) % MOD;
} else {
int l = i + 1, r = j - 1;
while (l <= r && s.charAt(l) != s.charAt(i)) {
++l;
}
while (r >= l && s.charAt(r) != s.charAt(j)) {
--r;
}
long inner = dp[i + 1][j - 1];
if (l > r) {
dp[i][j] = (int) ((inner * 2 + 2) % MOD);
} else if (l == r) {
dp[i][j] = (int) ((inner * 2 + 1) % MOD);
} else {
dp[i][j] = (int) ((inner * 2 - dp[l + 1][r - 1] + MOD) % MOD);
}
}
}
}
return dp[0][n - 1];
}
// 解法二:记忆化搜索
private long[][] memo;
private String str;
public int countPalindromicSubsequences2(String s) {
str = s;
int n = s.length();
memo = new long[n][n];
for (long[] row : memo) {
java.util.Arrays.fill(row, -1);
}
return (int) dfs(0, n - 1);
}
private long dfs(int i, int j) {
if (i > j) {
return 0;
}
if (i == j) {
return 1;
}
if (memo[i][j] != -1) {
return memo[i][j];
}
long res;
if (str.charAt(i) != str.charAt(j)) {
res = (dfs(i + 1, j) + dfs(i, j - 1) - dfs(i + 1, j - 1) + MOD) % MOD;
} else {
int l = i + 1, r = j - 1;
while (l <= r && str.charAt(l) != str.charAt(i)) {
++l;
}
while (r >= l && str.charAt(r) != str.charAt(j)) {
--r;
}
long inner = dfs(i + 1, j - 1);
if (l > r) {
res = (inner * 2 + 2) % MOD;
} else if (l == r) {
res = (inner * 2 + 1) % MOD;
} else {
res = (inner * 2 - dfs(l + 1, r - 1) + MOD) % MOD;
}
}
memo[i][j] = res;
return res;
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.countPalindromicSubsequences("bccb") == 6) ++pass; else System.out.println("FAIL test1: " + sol.countPalindromicSubsequences("bccb"));
++total; if (sol.countPalindromicSubsequences("abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba") == 104860361) ++pass; else System.out.println("FAIL test2");
++total; if (sol.countPalindromicSubsequences("a") == 1) ++pass; else System.out.println("FAIL test3");
++total; if (sol.countPalindromicSubsequences("aaa") == 3) ++pass; else System.out.println("FAIL test4: " + sol.countPalindromicSubsequences("aaa"));
++total; if (sol.countPalindromicSubsequences2("bccb") == 6) ++pass; else System.out.println("FAIL test5");
++total; if (sol.countPalindromicSubsequences2("aaa") == 3) ++pass; else System.out.println("FAIL test6");
++total; if (sol.countPalindromicSubsequences2("a") == 1) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0730 PASS ==");
} else {
System.out.println("== 0730 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n²) | O(n²) |
| 解法二 | O(n²) | O(n²) |
731. My Calendar II(我的日程安排表 II)
题目描述
实现一个程序来存放你的日程安排。如果要添加的时间内不会导致三重预订时,则可以存储这个
新的日程安排。
当三个日程安排有一些时间上的交叉时(例如三个日程安排都在同一时间内),就会产生三重预订。
事件能够用一对整数 startTime 和 endTime 表示,在一个半开区间的时间 [startTime, endTime)
上预定。
实现 MyCalendarTwo 类:
MyCalendarTwo()初始化日历对象。boolean book(int startTime, int endTime)如果可以将日程安排成功添加到日历中而不会导致
三重预订,返回true。否则,返回false并且不要将该日程安排添加到日历中。
示例 1
- 输入:
["MyCalendarTwo", "book", "book", "book", "book", "book", "book"]
[[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]] - 输出:
[null, true, true, true, false, true, true] - 解释:
MyCalendarTwo myCalendarTwo = new MyCalendarTwo();
myCalendarTwo.book(10, 20); // 返回 True
myCalendarTwo.book(50, 60); // 返回 True
myCalendarTwo.book(10, 40); // 返回 True
myCalendarTwo.book(5, 15); // 返回 False(三重预订:5, 10, 15)
myCalendarTwo.book(5, 10); // 返回 True
myCalendarTwo.book(25, 55); // 返回 True(25, 50, 55 形成双重预订,但不会三重)
提示: 0 <= start < end <= 10^9- 每个测试用例,调用
book方法最多不超过10^4次
解题思路
解法一(差分 + 扫描,主提交):用 TreeMap<Integer, Integer> 记录“变化量”差分:
每个事件的开始 +1、结束 -1。book 时先临时应用该事件的差分,扫描所有变化点累加,
若出现累计值 >= 3 则说明会导致三重预订,回滚并返回 false,否则提交。扫描累计用
TreeMap 的 keySet 顺序保证时间有序。时间复杂度 O(n²)(每次 book 扫描一遍),空间 O(n)。
解法二(双列表:已订 + 重叠区):维护 booked(全部已订区间)与 overlaps(已出现双重
预订的区间)。新事件与 overlaps 有交则拒绝;否则把与 booked 产生的重叠加入 overlaps,
把事件加入 booked。时间复杂度 O(n²),空间 O(n)。
代码实现
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
class MyCalendarTwo {
// ===== 解法一:差分 =====
private final TreeMap<Integer, Integer> diff = new TreeMap<>();
public MyCalendarTwo() {
}
public boolean book(int startTime, int endTime) {
diff.merge(startTime, 1, Integer::sum);
diff.merge(endTime, -1, Integer::sum);
int cnt = 0;
for (int v : diff.values()) {
cnt += v;
if (cnt >= 3) {
// 回滚
diff.merge(startTime, -1, Integer::sum);
diff.merge(endTime, 1, Integer::sum);
if (diff.get(startTime) == 0) diff.remove(startTime);
if (diff.get(endTime) == 0) diff.remove(endTime);
return false;
}
}
return true;
}
// ===== 解法二:双列表 =====
private static class MyCalendarTwo2 {
private final List<int[]> booked = new ArrayList<>();
private final List<int[]> overlaps = new ArrayList<>();
boolean book(int start, int end) {
for (int[] ov : overlaps) {
if (start < ov[1] && ov[0] < end) {
return false;
}
}
for (int[] b : booked) {
if (start < b[1] && b[0] < end) {
overlaps.add(new int[]{Math.max(start, b[0]), Math.min(end, b[1])});
}
}
booked.add(new int[]{start, end});
return true;
}
}
// ===== 本地测试 =====
public static void main(String[] args) {
int pass = 0, total = 0;
MyCalendarTwo mc = new MyCalendarTwo();
++total; if (mc.book(10, 20)) ++pass; else System.out.println("FAIL test1");
++total; if (mc.book(50, 60)) ++pass; else System.out.println("FAIL test2");
++total; if (mc.book(10, 40)) ++pass; else System.out.println("FAIL test3");
++total; if (!mc.book(5, 15)) ++pass; else System.out.println("FAIL test4");
++total; if (mc.book(5, 10)) ++pass; else System.out.println("FAIL test5");
++total; if (mc.book(25, 55)) ++pass; else System.out.println("FAIL test6");
MyCalendarTwo2 mc2 = new MyCalendarTwo2();
++total; if (mc2.book(10, 20) && mc2.book(50, 60) && mc2.book(10, 40) && !mc2.book(5, 15) && mc2.book(5, 10) && mc2.book(25, 55)) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0731 PASS ==");
} else {
System.out.println("== 0731 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n²) | O(n) |
| 解法二 | O(n²) | O(n) |
732. My Calendar III(我的日程安排表 III)
题目描述
当 k 个日程存在一些非空交集时(即 k 个日程包含了一些相同时间),就会产生 k 次预订。
给你一些日程安排 [startTime, endTime),请你在每个日程安排添加后,返回一个整数 k,
表示所有先前日程安排会产生的最大 k 次预订。
实现一个 MyCalendarThree 类来存放你的日程安排,你可以一直添加新的日程安排。
MyCalendarThree()初始化对象。int book(int startTime, int endTime)返回一个整数k,表示日历中存在的k次预订的
最大值。
示例
- 输入:
["MyCalendarThree", "book", "book", "book", "book", "book", "book"]
[[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]] - 输出:
[null, 1, 1, 2, 3, 3, 3] - 解释:
MyCalendarThree myCalendarThree = new MyCalendarThree();
myCalendarThree.book(10, 20); // 返回 1
myCalendarThree.book(50, 60); // 返回 1
myCalendarThree.book(10, 40); // 返回 2
myCalendarThree.book(5, 15); // 返回 3
myCalendarThree.book(5, 10); // 返回 3
myCalendarThree.book(25, 55); // 返回 3
提示: 0 <= startTime < endTime <= 10^9- 每个测试用例,调用
book函数最多不超过400次
解题思路
解法一(差分 + 全局最大,主提交):用 TreeMap<Integer, Integer> 记录时间点上的变化量:
开始 +1、结束 -1。每次 book 应用变化后,按 key 升序遍历累加,同时维护累计最大值,
返回最大重叠数。坐标可达 1e9,但事件数仅 400,TreeMap 节点数是线性的。
时间复杂度 O(n²)(每次 book 扫描一遍),空间复杂度 O(n)。
解法二(动态开点线段树 + 区间加):维护区间最大覆盖次数,query 根节点值即为全局最大。
时间复杂度 O(log C)(C 为坐标范围),空间 O(n log C)。
代码实现
import java.util.Map;
import java.util.TreeMap;
class MyCalendarThree {
// ===== 解法一:差分 =====
private final TreeMap<Integer, Integer> diff = new TreeMap<>();
public MyCalendarThree() {
}
public int book(int startTime, int endTime) {
diff.merge(startTime, 1, Integer::sum);
diff.merge(endTime, -1, Integer::sum);
int cur = 0, mx = 0;
for (int v : diff.values()) {
cur += v;
mx = Math.max(mx, cur);
}
return mx;
}
// ===== 解法二:动态开点线段树 =====
private static class Node {
Node left, right;
int l, r, mid;
int v; // 区间最大覆盖数
int add; // 懒标记
Node(int l, int r) {
this.l = l;
this.r = r;
this.mid = (l + r) >> 1;
}
}
private static class SegTree {
private final Node root = new Node(0, 1_000_000_000);
void update(int l, int r, int v) {
update(l, r, v, root);
}
private void update(int l, int r, int v, Node node) {
if (l <= node.l && node.r <= r) {
node.v += v;
node.add += v;
return;
}
pushdown(node);
if (l <= node.mid) {
update(l, r, v, node.left);
}
if (r > node.mid) {
update(l, r, v, node.right);
}
node.v = Math.max(node.left.v, node.right.v);
}
int queryMax() {
return root.v;
}
private void pushdown(Node node) {
if (node.left == null) {
node.left = new Node(node.l, node.mid);
}
if (node.right == null) {
node.right = new Node(node.mid + 1, node.r);
}
if (node.add != 0) {
node.left.v += node.add;
node.right.v += node.add;
node.left.add += node.add;
node.right.add += node.add;
node.add = 0;
}
}
}
private static class MyCalendarThree2 {
private final SegTree tree = new SegTree();
int book(int start, int end) {
tree.update(start, end - 1, 1);
return tree.queryMax();
}
}
// ===== 本地测试 =====
public static void main(String[] args) {
int pass = 0, total = 0;
MyCalendarThree mc = new MyCalendarThree();
++total; if (mc.book(10, 20) == 1) ++pass; else System.out.println("FAIL test1");
++total; if (mc.book(50, 60) == 1) ++pass; else System.out.println("FAIL test2");
++total; if (mc.book(10, 40) == 2) ++pass; else System.out.println("FAIL test3: " + mc.book(10, 40));
++total; if (mc.book(5, 15) == 3) ++pass; else System.out.println("FAIL test4: " + mc.book(5, 15));
++total; if (mc.book(5, 10) == 3) ++pass; else System.out.println("FAIL test5");
++total; if (mc.book(25, 55) == 3) ++pass; else System.out.println("FAIL test6");
MyCalendarThree2 mc2 = new MyCalendarThree2();
++total; if (mc2.book(10, 20) == 1 && mc2.book(50, 60) == 1 && mc2.book(10, 40) == 2
&& mc2.book(5, 15) == 3 && mc2.book(5, 10) == 3 && mc2.book(25, 55) == 3) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0732 PASS ==");
} else {
System.out.println("== 0732 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n²)(每 book 扫一次) | O(n) |
| 解法二 | O(n log C) | O(n log C) |
733. Flood Fill(图像渲染)
题目描述
有一幅以 m x n 的二维整数数组表示的图画 image,其中 image[i][j] 表示该图画的像素
值大小。给你三个整数 sr、sc 和 color,从像素 image[sr][sc] 开始对图像进行上色填充。
为了完成上色工作:
- 从初始像素开始,将其颜色改为
color; - 对初始坐标的上下左右四个方向上相邻且与初始像素的原始颜色同色的像素点执行相同操作;
- 通过检查与初始像素的原始颜色相同的相邻像素并修改其颜色来继续重复此过程;
- 当没有其它原始颜色的相邻像素时停止操作。
最后返回经过上色渲染修改后的图像。
示例 1
- 输入:image = [[1,1,1],[1,1,0],[1,0,1]],sr = 1, sc = 1, color = 2
- 输出:[[2,2,2],[2,2,0],[2,0,1]]
- 解释:在图像的正中间,坐标
(sr,sc)=(1,1)(即红色像素),在路径上所有符合条件的像素点的
颜色都被更改成相同的新颜色。注意,右下角的像素没有更改为 2,因为它不是在上下左右四个方向
上与初始点相连的像素点。
示例 2
- 输入:image = [[0,0,0],[0,0,0]],sr = 0, sc = 0, color = 0
- 输出:[[0,0,0],[0,0,0]]
- 解释:初始像素已经用 0 着色,这与目标颜色相同,不会对图像进行任何更改。
提示: m == image.lengthn == image[i].length1 <= m, n <= 500 <= image[i][j], color < 2^160 <= sr < m0 <= sc < n
解题思路
解法一(DFS,主提交):记初始像素颜色为 oc,若 oc != color 则从 (sr, sc) 开始深度优先
搜索,把仍为 oc 的四邻像素改为 color。染色同时充当访问标记,不必额外判重。
时间复杂度 O(m·n),空间复杂度 O(m·n)(递归栈深度)。
解法二(BFS):用队列从起点开始层序扩展,弹出像素后把仍为 oc 的四邻染成目标色并入队。
避免递归栈溢出,渐近复杂度相同。
代码实现
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
class Solution {
// 解法一:DFS
public int[][] floodFill(int[][] image, int sr, int sc, int color) {
int oc = image[sr][sc];
if (oc != color) {
dfs(image, sr, sc, oc, color);
}
return image;
}
private void dfs(int[][] image, int i, int j, int oc, int color) {
if (i < 0 || i >= image.length || j < 0 || j >= image[0].length
|| image[i][j] != oc) {
return;
}
image[i][j] = color;
dfs(image, i - 1, j, oc, color);
dfs(image, i + 1, j, oc, color);
dfs(image, i, j - 1, oc, color);
dfs(image, i, j + 1, oc, color);
}
// 解法二:BFS
public int[][] floodFill2(int[][] image, int sr, int sc, int color) {
if (image[sr][sc] == color) {
return image;
}
int oc = image[sr][sc];
int m = image.length, n = image[0].length;
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[] {sr, sc});
image[sr][sc] = color;
int[] di = {-1, 1, 0, 0};
int[] dj = {0, 0, -1, 1};
while (!q.isEmpty()) {
int[] p = q.poll();
for (int k = 0; k < 4; ++k) {
int x = p[0] + di[k], y = p[1] + dj[k];
if (x >= 0 && x < m && y >= 0 && y < n && image[x][y] == oc) {
image[x][y] = color;
q.offer(new int[] {x, y});
}
}
}
return image;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] img = {{1, 1, 1}, {1, 1, 0}, {1, 0, 1}};
int[][] out1 = sol.floodFill(img, 1, 1, 2);
++total;
if (Arrays.deepToString(out1).equals("[[2, 2, 2], [2, 2, 0], [2, 0, 1]]")) ++pass;
else System.out.println("FAIL test1 got " + Arrays.deepToString(out1));
int[][] img2 = {{0, 0, 0}, {0, 0, 0}};
int[][] out2 = sol.floodFill(img2, 0, 0, 0);
++total;
if (Arrays.deepToString(out2).equals("[[0, 0, 0], [0, 0, 0]]")) ++pass;
else System.out.println("FAIL test2 got " + Arrays.deepToString(out2));
int[][] img3 = {{1, 1, 1}, {1, 1, 0}, {1, 0, 1}};
int[][] out3 = sol.floodFill2(img3, 1, 1, 2);
++total;
if (Arrays.deepToString(out3).equals("[[2, 2, 2], [2, 2, 0], [2, 0, 1]]")) ++pass;
else System.out.println("FAIL test3 got " + Arrays.deepToString(out3));
if (pass == total) {
System.out.println("== 0733 PASS ==");
} else {
System.out.println("== 0733 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m·n) | O(m·n) |
| 解法二 | O(m·n) | O(m·n) |
734. Sentence Similarity(句子相似性)
题目描述
我们可以将一个句子表示为一个单词数组,例如句子 "I am happy with leetcode" 可以表示为
arr = ["I","am","happy","with","leetcode"]。
给定两个句子 sentence1 和 sentence2 分别表示为一个字符串数组,并给定一个字符串对
similarPairs,其中 similarPairs[i] = [xi, yi] 表示两个单词 xi 和 yi 是相似的。
如果 sentence1 和 sentence2 相似则返回 true,否则返回 false。
两个句子是相似的,如果:
- 它们具有相同的长度(即相同的字数);
sentence1[i]和sentence2[i]是相似的。
请注意,一个词总是与它自己相似,也请注意,相似关系是【不可传递】的。例如,如果单词a和
b是相似的,单词b和c也是相似的,那么a和c不一定相似。
示例 1
- 输入:sentence1 = ["great","acting","skills"], sentence2 = ["fine","drama","talent"],
similarPairs = [["great","fine"],["drama","acting"],["skills","talent"]] - 输出:true
示例 2
- 输入:sentence1 = ["great"], sentence2 = ["great"], similarPairs = []
- 输出:true
示例 3
- 输入:sentence1 = ["great"], sentence2 = ["doubleplus","good"], similarPairs = [["great","doubleplus"]]
- 输出:false
提示: 1 <= sentence1.length, sentence2.length <= 10001 <= sentence1[i].length, sentence2[i].length <= 200 <= similarPairs.length <= 2000similarPairs[i]的长度为 2- 所有单词只由英文大写字母和小写字母组成
解题思路
解法一(HashSet 存无序对,主提交):把每对相似单词 (x, y) 连同反转 (y, x) 存入
HashSet<String>(用 x + "#" + y 编码)。遍历时若两个单词相同则直接相似,否则检查
a + "#" + b 或 b + "#" + a 是否在集合中。因为相似不可传递,直接查对即可。
时间复杂度 O(n + m),空间复杂度 O(p)(p 为相似对数量)。
解法二(Map 邻接表查询):用 Map<String, Set<String>> 记录每个单词的相似邻居,判断时
查 adj.containsKey(a) && adj.get(a).contains(b)。逻辑等价,空间略大。
代码实现
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class Solution {
// 解法一:无序对哈希
public boolean areSentencesSimilar(String[] sentence1, String[] sentence2,
List<List<String>> similarPairs) {
if (sentence1.length != sentence2.length) {
return false;
}
Set<String> pairs = new HashSet<>();
for (List<String> p : similarPairs) {
pairs.add(p.get(0) + "#" + p.get(1));
pairs.add(p.get(1) + "#" + p.get(0));
}
for (int i = 0; i < sentence1.length; ++i) {
String a = sentence1[i], b = sentence2[i];
if (a.equals(b)) {
continue;
}
if (!pairs.contains(a + "#" + b)) {
return false;
}
}
return true;
}
// 解法二:Map 邻居查询
public boolean areSentencesSimilar2(String[] sentence1, String[] sentence2,
List<List<String>> similarPairs) {
if (sentence1.length != sentence2.length) {
return false;
}
java.util.Map<String, Set<String>> adj = new java.util.HashMap<>();
for (List<String> p : similarPairs) {
adj.computeIfAbsent(p.get(0), k -> new HashSet<>()).add(p.get(1));
adj.computeIfAbsent(p.get(1), k -> new HashSet<>()).add(p.get(0));
}
for (int i = 0; i < sentence1.length; ++i) {
String a = sentence1[i], b = sentence2[i];
if (a.equals(b)) {
continue;
}
Set<String> ns = adj.get(a);
if (ns == null || !ns.contains(b)) {
return false;
}
}
return true;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
String[] s1 = {"great", "acting", "skills"};
String[] s2 = {"fine", "drama", "talent"};
List<List<String>> pairs = java.util.Arrays.asList(
java.util.Arrays.asList("great", "fine"),
java.util.Arrays.asList("drama", "acting"),
java.util.Arrays.asList("skills", "talent"));
++total; if (sol.areSentencesSimilar(s1, s2, pairs)) ++pass; else System.out.println("FAIL test1");
++total; if (sol.areSentencesSimilar2(s1, s2, pairs)) ++pass; else System.out.println("FAIL test2");
String[] s3 = {"great"};
String[] s4 = {"great"};
++total; if (sol.areSentencesSimilar(s3, s4, new java.util.ArrayList<>())) ++pass; else System.out.println("FAIL test3");
String[] s5 = {"great"};
String[] s6 = {"doubleplus", "good"};
List<List<String>> pairs2 = java.util.Arrays.asList(java.util.Arrays.asList("great", "doubleplus"));
++total; if (!sol.areSentencesSimilar(s5, s6, pairs2)) ++pass; else System.out.println("FAIL test4");
++total; if (!sol.areSentencesSimilar2(s5, s6, pairs2)) ++pass; else System.out.println("FAIL test5");
if (pass == total) {
System.out.println("== 0734 PASS ==");
} else {
System.out.println("== 0734 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + p) | O(p) |
| 解法二 | O(n + p) | O(p) |
735. Asteroid Collision(小行星碰撞)
题目描述
给定一个整数数组 asteroids,表示在同一行的小行星。数组中小行星的索引表示它们在空间中
的相对位置。对于数组中的每一个元素,其绝对值表示小行星的大小,正负表示小行星的移动方向
(正表示向右移动,负表示向左移动)。每一颗小行星以相同的速度移动。
找出碰撞后剩下的所有小行星。碰撞规则:两个小行星相互碰撞,较小的小行星会爆炸。如果两颗
小行星大小相同,则两颗小行星都会爆炸。两颗移动方向相同的小行星,永远不会发生碰撞。
示例 1
- 输入:asteroids = [5,10,-5]
- 输出:[5,10]
- 解释:10 和 -5 碰撞后只剩下 10。5 和 10 永远不会发生碰撞。
示例 2
- 输入:asteroids = [8,-8]
- 输出:[]
示例 3
- 输入:asteroids = [10,2,-5]
- 输出:[10]
示例 4
- 输入:asteroids = [3,5,-6,2,-1,4]
- 输出:[-6,2,4]
提示: 2 <= asteroids.length <= 10^4-1000 <= asteroids[i] <= 1000asteroids[i] != 0
解题思路
解法一(栈模拟碰撞,主提交):只有「栈顶向右(正数)且当前向左(负数)」才会碰撞。用数组
模拟栈,top 指向栈顶元素。当前为负数时,循环把绝对值小于 -cur 的栈顶弹掉;若栈顶绝对值
相等则双双消失(弹掉栈顶且当前也消失);若栈顶绝对值更大则当前小行星爆炸。其余情况直接入栈。
时间复杂度 O(n),空间复杂度 O(n)。
解法二(Deque 显式栈):逻辑与解法一完全一致,只是用 Deque<Integer> 存储,最后倒序拷贝
到结果数组。
代码实现
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
class Solution {
// 解法一:数组模拟栈
public int[] asteroidCollision(int[] asteroids) {
int n = asteroids.length;
int[] stk = new int[n];
int top = 0;
for (int cur : asteroids) {
boolean destroyed = false;
while (top > 0 && stk[top - 1] > 0 && cur < 0) {
int prev = stk[top - 1];
if (prev < -cur) {
--top; // 栈顶被撞毁,继续比较
} else if (prev == -cur) {
--top; // 两者都毁
destroyed = true;
break;
} else {
destroyed = true; // 当前被撞毁
break;
}
}
if (!destroyed) {
stk[top++] = cur;
}
}
return Arrays.copyOf(stk, top);
}
// 解法二:Deque 显式栈
public int[] asteroidCollision2(int[] asteroids) {
Deque<Integer> stk = new ArrayDeque<>();
for (int cur : asteroids) {
boolean alive = true;
while (!stk.isEmpty() && stk.peek() > 0 && cur < 0) {
int prev = stk.peek();
if (prev < -cur) {
stk.pop();
} else if (prev == -cur) {
stk.pop();
alive = false;
break;
} else {
alive = false;
break;
}
}
if (alive) {
stk.push(cur);
}
}
int[] res = new int[stk.size()];
for (int i = res.length - 1; i >= 0; --i) {
res[i] = stk.pop();
}
return res;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] a1 = {5, 10, -5};
++total; if (Arrays.equals(sol.asteroidCollision(a1), new int[] {5, 10})) ++pass; else System.out.println("FAIL test1 " + Arrays.toString(sol.asteroidCollision(a1)));
int[] a2 = {8, -8};
++total; if (Arrays.equals(sol.asteroidCollision(a2), new int[0])) ++pass; else System.out.println("FAIL test2 " + Arrays.toString(sol.asteroidCollision(a2)));
int[] a3 = {10, 2, -5};
++total; if (Arrays.equals(sol.asteroidCollision(a3), new int[] {10})) ++pass; else System.out.println("FAIL test3 " + Arrays.toString(sol.asteroidCollision(a3)));
int[] a4 = {3, 5, -6, 2, -1, 4};
++total; if (Arrays.equals(sol.asteroidCollision(a4), new int[] {-6, 2, 4})) ++pass; else System.out.println("FAIL test4 " + Arrays.toString(sol.asteroidCollision(a4)));
int[] a5 = {5, 10, -5};
++total; if (Arrays.equals(sol.asteroidCollision2(a5), new int[] {5, 10})) ++pass; else System.out.println("FAIL test5 " + Arrays.toString(sol.asteroidCollision2(a5)));
int[] a6 = {3, 5, -6, 2, -1, 4};
++total; if (Arrays.equals(sol.asteroidCollision2(a6), new int[] {-6, 2, 4})) ++pass; else System.out.println("FAIL test6 " + Arrays.toString(sol.asteroidCollision2(a6)));
if (pass == total) {
System.out.println("== 0735 PASS ==");
} else {
System.out.println("== 0735 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n) | O(n) |
736. Parse Lisp Expression(Lisp 语法解析)
题目描述
给你一个类似 Lisp 语句的字符串表达式 expression,求出其计算结果。表达式语法如下:
- 表达式可以为整数、
let表达式、add表达式、mult表达式,或赋值的变量。表达式的结果
总是一个整数(整数可以是正整数、负整数、0)。 let表达式采用"(let v1 e1 v2 e2 ... vn en expr)"的形式,let后面跟随一对或多对
交替的变量和表达式:变量v1被赋值为表达式e1的值,依次类推;最终let表达式的值
为expr表达式的值。add表达式表示为"(add e1 e2)",结果是e1与e2的值之和。mult表达式表示为"(mult e1 e2)",结果是e1与e2的值之积。- 变量名以小写字符开始,之后跟随 0 个或多个小写字符或数字。
"add"、"let"、"mult"为
关键字,不会用作变量名。 - 作用域:计算变量名时,首先检查最内层作用域(按括号计),然后依次向外检查外部作用域。
测试用例中每一个表达式都是合法的。
示例 1
- 输入:expression = "(let x 2 (mult x (let x 3 y 4 (add x y))))"
- 输出:14
示例 2
- 输入:expression = "(let x 3 x 2 x)"
- 输出:2
示例 3
- 输入:expression = "(let x 1 y 2 x (add x y) (add x y))"
- 输出:5
示例 4
- 输入:expression = "(let x 2 (add (let x 3 (let x 4 x)) x))"
- 输出:6
提示: - 给定表达式
expression的长度不超过 2000 - 表达式中只包含小写字母、数字、
'('、')'和空格
解题思路
解法一(递归下降 + 作用域 Map,主提交):用全局下标 ptr 顺序扫描。next() 读取下一个
token;eval(scope) 求值一个表达式:
- 若以
(开头,读关键字:add求两子表达式之和,mult求两子表达式之积;let循环读 「变量 + 值表达式」对——若下一个 token 是数字或(开头的子表达式,则它是最终结果表达式; 若读到变量名后紧跟),则该变量就是let的结果;否则对变量赋值后继续循环。 - 否则读普通 token:数字直接解析,变量从作用域中取值。
作用域通过「拷贝父 Map 成局部 Map」实现,
let内部的新绑定只存在于局部 Map,函数返回时 自然消失,从而天然支持嵌套作用域的遮蔽。 时间复杂度 O(n),空间复杂度 O(n)。
代码实现
import java.util.HashMap;
import java.util.Map;
class Solution {
private String expr;
private int ptr;
public int evaluate(String expression) {
expr = expression;
ptr = 0;
return eval(new HashMap<String, Integer>());
}
private int eval(Map<String, Integer> scope) {
ptr = skipBlank(ptr);
if (expr.charAt(ptr) != '(') {
String tok = next();
if (tok.charAt(0) == '-' || Character.isDigit(tok.charAt(0))) {
return Integer.parseInt(tok);
}
return scope.get(tok);
}
++ptr; // 跳过 '('
String op = next();
int result;
if (op.equals("add")) {
int a = eval(scope);
int b = eval(scope);
result = a + b;
} else if (op.equals("mult")) {
int a = eval(scope);
int b = eval(scope);
result = a * b;
} else { // let
Map<String, Integer> local = new HashMap<>(scope);
while (true) {
ptr = skipBlank(ptr);
char c = expr.charAt(ptr);
if (c == '(' || c == '-' || Character.isDigit(c)) {
result = eval(local);
break;
}
String var = next(); // 变量名(以字母开头)
ptr = skipBlank(ptr);
if (ptr < expr.length() && expr.charAt(ptr) == ')') {
// 变量后无值表达式:该变量即为 let 结果
result = local.get(var);
break;
}
int val = eval(local);
local.put(var, val);
}
}
ptr = skipBlank(ptr);
if (ptr < expr.length() && expr.charAt(ptr) == ')') {
++ptr; // 跳过本表达式对应的右括号
}
return result;
}
private int skipBlank(int p) {
while (p < expr.length() && expr.charAt(p) == ' ') {
++p;
}
return p;
}
private String next() {
ptr = skipBlank(ptr);
int s = ptr;
while (ptr < expr.length() && expr.charAt(ptr) != ' ' && expr.charAt(ptr) != '('
&& expr.charAt(ptr) != ')') {
++ptr;
}
return expr.substring(s, ptr);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.evaluate("(let x 2 (mult x (let x 3 y 4 (add x y))))") == 14) ++pass; else System.out.println("FAIL test1 got " + sol.evaluate("(let x 2 (mult x (let x 3 y 4 (add x y))))"));
++total; if (sol.evaluate("(let x 3 x 2 x)") == 2) ++pass; else System.out.println("FAIL test2");
++total; if (sol.evaluate("(let x 1 y 2 x (add x y) (add x y))") == 5) ++pass; else System.out.println("FAIL test3");
++total; if (sol.evaluate("(let x 2 (add (let x 3 (let x 4 x)) x))") == 6) ++pass; else System.out.println("FAIL test4");
++total; if (sol.evaluate("(mult 3 (add 2 3))") == 15) ++pass; else System.out.println("FAIL test5");
++total; if (sol.evaluate("(let a 5 (mult a 2))") == 10) ++pass; else System.out.println("FAIL test6");
++total; if (sol.evaluate("(let x -1 x)") == -1) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0736 PASS ==");
} else {
System.out.println("== 0736 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
737. Sentence Similarity II(句子相似性 II)
题目描述
我们可以将一个句子表示为一个单词数组,例如句子 I am happy with leetcode 可以表示为
arr = ["I","am","happy","with","leetcode"]。
给定两个句子 sentence1 和 sentence2 分别表示为一个字符串数组,并给定一个字符串对
similarPairs,其中 similarPairs[i] = [xi, yi] 表示两个单词 xi 和 yi 是相似的。
两个句子是相似的,如果:
- 它们具有相同的长度(即相同的词数);
sentence1[i]和sentence2[i]是相似的。 请注意,一个词总是与它自己相似,也请注意,相似关系是【可传递】的。例如,如果单词a和b是相似的,单词b和c也是相似的,那么a和c也是相似的。
示例 1
- 输入:sentence1 = ["great","acting","skills"], sentence2 = ["fine","drama","talent"], similarPairs = [["great","good"],["fine","good"],["drama","acting"],["skills","talent"]]
- 输出:true
示例 2
- 输入:sentence1 = ["I","love","leetcode"], sentence2 = ["I","love","onepiece"], similarPairs = [["manga","onepiece"],["platform","anime"],["leetcode","platform"],["anime","manga"]]
- 输出:true
示例 3
- 输入:sentence1 = ["I","love","leetcode"], sentence2 = ["I","love","onepiece"], similarPairs = [["manga","hunterXhunter"],["platform","anime"],["leetcode","platform"],["anime","manga"]]
- 输出:false 提示:
1 <= sentence1.length, sentence2.length <= 10001 <= sentence1[i].length, sentence2[i].length <= 200 <= similarPairs.length <= 2000similarPairs[i]的长度为 2- 所有单词只由英文大写字母和小写字母组成
解题思路
解法一(并查集,主提交):相似关系可传递,天然适合并查集。把每个单词映射为一个 id(用
Map<String, Integer>),将每对相似单词 union 到同一集合。判断两个句子相似时,逐位检查
sentence1[i] 与 sentence2[i] 是否同词或 find(a) == find(b)。
时间复杂度 O((n + p)·α),空间复杂度 O(p)。
解法二(DFS 建图遍历):把相似关系建成无向图(Map<String, List<String>>),判断两个词
是否相似时以其中一个为起点 DFS,看能否到达另一个词。可传递性由可达性自然保证。
代码实现
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {
// 解法一:并查集
private Map<String, String> parent = new HashMap<>();
private String find(String x) {
if (!parent.containsKey(x)) {
parent.put(x, x);
}
if (!parent.get(x).equals(x)) {
parent.put(x, find(parent.get(x)));
}
return parent.get(x);
}
private void union(String a, String b) {
String ra = find(a), rb = find(b);
if (!ra.equals(rb)) {
parent.put(ra, rb);
}
}
public boolean areSentencesSimilarTwo(String[] sentence1, String[] sentence2,
List<List<String>> similarPairs) {
if (sentence1.length != sentence2.length) {
return false;
}
parent.clear();
for (List<String> p : similarPairs) {
union(p.get(0), p.get(1));
}
for (int i = 0; i < sentence1.length; ++i) {
String a = sentence1[i], b = sentence2[i];
if (!a.equals(b) && !find(a).equals(find(b))) {
return false;
}
}
return true;
}
// 解法二:DFS 建图
private Map<String, List<String>> graph = new HashMap<>();
public boolean areSentencesSimilarTwo2(String[] sentence1, String[] sentence2,
List<List<String>> similarPairs) {
if (sentence1.length != sentence2.length) {
return false;
}
graph.clear();
for (List<String> p : similarPairs) {
graph.computeIfAbsent(p.get(0), k -> new java.util.ArrayList<>()).add(p.get(1));
graph.computeIfAbsent(p.get(1), k -> new java.util.ArrayList<>()).add(p.get(0));
}
for (int i = 0; i < sentence1.length; ++i) {
String a = sentence1[i], b = sentence2[i];
if (!a.equals(b) && !reachable(a, b, new java.util.HashSet<String>())) {
return false;
}
}
return true;
}
private boolean reachable(String s, String t, java.util.Set<String> seen) {
if (s.equals(t)) {
return true;
}
seen.add(s);
List<String> ns = graph.get(s);
if (ns == null) {
return false;
}
for (String nb : ns) {
if (!seen.contains(nb) && reachable(nb, t, seen)) {
return true;
}
}
return false;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
String[] s1 = {"great", "acting", "skills"};
String[] s2 = {"fine", "drama", "talent"};
List<List<String>> p1 = Arrays.asList(
Arrays.asList("great", "good"), Arrays.asList("fine", "good"),
Arrays.asList("drama", "acting"), Arrays.asList("skills", "talent"));
++total; if (sol.areSentencesSimilarTwo(s1, s2, p1)) ++pass; else System.out.println("FAIL test1");
++total; if (sol.areSentencesSimilarTwo2(s1, s2, p1)) ++pass; else System.out.println("FAIL test2");
String[] s3 = {"I", "love", "leetcode"};
String[] s4 = {"I", "love", "onepiece"};
List<List<String>> p2 = Arrays.asList(
Arrays.asList("manga", "onepiece"), Arrays.asList("platform", "anime"),
Arrays.asList("leetcode", "platform"), Arrays.asList("anime", "manga"));
++total; if (sol.areSentencesSimilarTwo(s3, s4, p2)) ++pass; else System.out.println("FAIL test3");
++total; if (sol.areSentencesSimilarTwo2(s3, s4, p2)) ++pass; else System.out.println("FAIL test4");
List<List<String>> p3 = Arrays.asList(
Arrays.asList("manga", "hunterXhunter"), Arrays.asList("platform", "anime"),
Arrays.asList("leetcode", "platform"), Arrays.asList("anime", "manga"));
++total; if (!sol.areSentencesSimilarTwo(s3, s4, p3)) ++pass; else System.out.println("FAIL test5");
++total; if (!sol.areSentencesSimilarTwo2(s3, s4, p3)) ++pass; else System.out.println("FAIL test6");
if (pass == total) {
System.out.println("== 0737 PASS ==");
} else {
System.out.println("== 0737 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O((n + p)·α) | O(p) |
| 解法二 | O(n·(p + w)) | O(p) |
738. Monotone Increasing Digits(单调递增的数字)
题目描述
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n,返回小于或等于 n 的最大数字,且数字呈单调递增。
示例 1
- 输入:n = 10
- 输出:9
示例 2
- 输入:n = 1234
- 输出:1234
示例 3
- 输入:n = 332
- 输出:299 提示:
0 <= n <= 10^9
解题思路
解法一(贪心 + 前缀修正,主提交):把 n 转成字符数组。从左往右找第一个满足
digits[i] > digits[i + 1] 的位置 i,说明必须让 i 位减 1 并把后面全部变 9 才能保持
不大于 n 且单调。但 digits[i] 减 1 后可能小于 digits[i - 1],需要不断向左回退:令
j = i,只要 j > 0 && digits[j] - 1 < digits[j - 1],就把 j 左移;最终把 digits[j]--,
然后 j + 1 之后全部置 '9'。
时间复杂度 O(len),len 为十进制位数(最多 10 位),空间复杂度 O(len)。
解法二(暴力枚举辅助思路):从 n 向下逐个检查单调性会超时,仅用于小数据自测佐证思路,
不作为主解法。
代码实现
class Solution {
// 解法一:贪心 + 前缀修正
public int monotoneIncreasingDigits(int n) {
char[] d = String.valueOf(n).toCharArray();
int len = d.length;
int i = 0;
while (i + 1 < len && d[i] <= d[i + 1]) {
++i;
}
if (i + 1 == len) {
return n; // 本身已单调递增
}
int j = i;
while (j > 0 && d[j] - 1 < d[j - 1]) {
--j;
}
d[j] = (char) (d[j] - 1);
for (int k = j + 1; k < len; ++k) {
d[k] = '9';
}
return Integer.parseInt(new String(d));
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.monotoneIncreasingDigits(10) == 9) ++pass; else System.out.println("FAIL test1 got " + sol.monotoneIncreasingDigits(10));
++total; if (sol.monotoneIncreasingDigits(1234) == 1234) ++pass; else System.out.println("FAIL test2");
++total; if (sol.monotoneIncreasingDigits(332) == 299) ++pass; else System.out.println("FAIL test3");
++total; if (sol.monotoneIncreasingDigits(999) == 999) ++pass; else System.out.println("FAIL test4");
++total; if (sol.monotoneIncreasingDigits(120) == 119) ++pass; else System.out.println("FAIL test5 got " + sol.monotoneIncreasingDigits(120));
++total; if (sol.monotoneIncreasingDigits(100) == 99) ++pass; else System.out.println("FAIL test6 got " + sol.monotoneIncreasingDigits(100));
++total; if (sol.monotoneIncreasingDigits(0) == 0) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0738 PASS ==");
} else {
System.out.println("== 0738 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(len) | O(len) |
739. Daily Temperatures(每日温度)
题目描述
给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i]
是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用
0 来代替。
示例 1
- 输入:temperatures = [73,74,75,71,69,72,76,73]
- 输出:[1,1,4,2,1,1,0,0]
示例 2
- 输入:temperatures = [30,40,50,60]
- 输出:[1,1,1,0]
示例 3
- 输入:temperatures = [30,60,90]
- 输出:[1,1,0] 提示:
1 <= temperatures.length <= 10^530 <= temperatures[i] <= 100
解题思路
解法一(单调递减栈,主提交):从右往左遍历,维护一个「温度单调递减」的下标栈(栈内下标对应
的温度从栈底到栈顶严格递减)。对当前 i,把栈中所有温度不高于 temperatures[i] 的下标弹掉,
剩下的栈顶就是右侧第一个比 temperatures[i] 高的位置,answer[i] = stk.peek() - i。
每个下标进出栈一次,时间复杂度 O(n),空间复杂度 O(n)。
解法二(从右往左朴素查找):对每个位置向右线性找第一个更高温度,最坏 O(n²),仅作对照。
代码实现
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
class Solution {
// 解法一:单调栈
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ans = new int[n];
Deque<Integer> stk = new ArrayDeque<>(); // 存下标,温度从栈底到栈顶严格递减
for (int i = n - 1; i >= 0; --i) {
while (!stk.isEmpty() && temperatures[stk.peek()] <= temperatures[i]) {
stk.pop();
}
ans[i] = stk.isEmpty() ? 0 : stk.peek() - i;
stk.push(i);
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] t1 = {73, 74, 75, 71, 69, 72, 76, 73};
++total; if (Arrays.equals(sol.dailyTemperatures(t1), new int[] {1, 1, 4, 2, 1, 1, 0, 0})) ++pass; else System.out.println("FAIL test1 " + Arrays.toString(sol.dailyTemperatures(t1)));
int[] t2 = {30, 40, 50, 60};
++total; if (Arrays.equals(sol.dailyTemperatures(t2), new int[] {1, 1, 1, 0})) ++pass; else System.out.println("FAIL test2");
int[] t3 = {30, 60, 90};
++total; if (Arrays.equals(sol.dailyTemperatures(t3), new int[] {1, 1, 0})) ++pass; else System.out.println("FAIL test3");
int[] t4 = {89, 62, 70, 58, 47, 47, 46, 76, 100, 70};
++total; if (Arrays.equals(sol.dailyTemperatures(t4), new int[] {8, 1, 5, 4, 3, 2, 1, 1, 0, 0})) ++pass; else System.out.println("FAIL test4 " + Arrays.toString(sol.dailyTemperatures(t4)));
if (pass == total) {
System.out.println("== 0739 PASS ==");
} else {
System.out.println("== 0739 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n²) | O(1) |