LeetCode 914-925. 题解合集
914. X of a Kind in a Deck of Cards(卡牌分组)
题目描述
给定一副牌,每张牌上都写着一个整数。
此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组:
- 每组都有
X张牌。 - 组内所有的牌上都写着相同的整数。
仅当你可选的X >= 2时返回true。
示例 1
- 输入:deck = [1,2,3,4,4,3,2,1]
- 输出:
true - 解释:可行的分组是 [1,1],[2,2],[3,3],[4,4]。
示例 2
- 输入:deck = [1,1,1,2,2,2,3,3]
- 输出:
false - 解释:没有满足要求的分组。
提示: 1 <= deck.length <= 10^40 <= deck[i] < 10^4
解题思路
每种数字的牌张数必须都能被同一个 X >= 2 整除,即所有出现次数的最大公约数 >= 2。
解法一(计数 + 最大公约数,主提交):统计每个数字出现次数,求这些次数的 gcd;
若 gcd >= 2 返回 true,否则 false。
解法二(枚举 X,对照实现):从 2 枚举到最小出现次数,检查每个出现次数是否能被 X 整除,
找到任意一个即返回 true;纯枚举便于理解但不高效,作为对照。
代码实现
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:计数 + 最大公约数(推荐提交)
public boolean hasGroupsSizeX(int[] deck) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int x : deck) {
cnt.put(x, cnt.getOrDefault(x, 0) + 1);
}
int g = 0;
for (int v : cnt.values()) {
g = gcd(g, v);
}
return g >= 2;
}
private int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
// 解法二:枚举 X(对照实现)
public boolean hasGroupsSizeX2(int[] deck) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int x : deck) {
cnt.put(x, cnt.getOrDefault(x, 0) + 1);
}
int min = deck.length;
for (int v : cnt.values()) {
min = Math.min(min, v);
}
outer:
for (int x = 2; x <= min; ++x) {
for (int v : cnt.values()) {
if (v % x != 0) {
continue outer;
}
}
return true;
}
return false;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] decks = {
{1, 2, 3, 4, 4, 3, 2, 1},
{1, 1, 1, 2, 2, 2, 3, 3},
{1},
{1, 1},
{1, 1, 2, 2, 2, 2},
{1, 1, 1, 1, 2, 2, 2, 2, 2, 2}
};
boolean[] exp = {true, false, false, true, true, true};
for (int t = 0; t < decks.length; ++t) {
++total;
boolean g1 = s.hasGroupsSizeX(decks[t]);
boolean g2 = s.hasGroupsSizeX2(decks[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 914-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0914 PASS ==");
} else {
System.out.println("== 0914 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(gcd) | O(n + k) | O(k)(k 为不同数字数) |
| 解法二(枚举 X) | O(n + minCnt * k) | O(k) |
915. Partition Array into Disjoint Intervals(分割数组)
题目描述
给定一个数组 nums,将其划分为两个连续子数组 left 和 right,使得:
left中的每个元素都小于或等于right中的每个元素。left和right都是非空的。left的长度要尽可能小。
在完成这样的分组后返回left的长度。
用例可以保证存在这样的划分方法。
示例 1
- 输入:
nums= [5,0,3,8,6] - 输出:3
- 解释:
left= [5,0,3],right= [8,6]。
示例 2
- 输入:
nums= [1,1,1,0,6,12] - 输出:4
- 解释:
left= [1,1,1,0],right= [6,12]。
提示: 2 <= nums.length <= 10^50 <= nums[i] <= 10^6- 可以保证至少有一种方法能够按题目所描述的那样对
nums进行划分。
解题思路
划分点 i(left 为 nums[0..i])合法当且仅当 left 的最大值 <= right 的最小值。
解法一(左右最值预处理,主提交):先算后缀最小值 sufMin[i](nums[i..n-1] 的最小
值),再从前往后维护前缀最大值 preMax,第一个满足 preMax <= sufMin[i+1] 的
i+1 即答案,O(n)。
解法二(一趟扫描 + 双值维护,对照实现):遍历时维护当前段最大值 curMax 与全局
最大值 maxSeen;若 nums[i] < maxSeen,说明 i 必须并入 left,更新
cut = i 并把 curMax 提升为 maxSeen;最终答案为 cut + 1。一趟完成。
代码实现
class Solution {
// 解法一:前缀最大 + 后缀最小(推荐提交)
public int partitionDisjoint(int[] nums) {
int n = nums.length;
int[] sufMin = new int[n];
sufMin[n - 1] = nums[n - 1];
for (int i = n - 2; i >= 0; --i) {
sufMin[i] = Math.min(nums[i], sufMin[i + 1]);
}
int preMax = nums[0];
for (int i = 0; i < n - 1; ++i) {
preMax = Math.max(preMax, nums[i]);
if (preMax <= sufMin[i + 1]) {
return i + 1;
}
}
return n; // 题目保证有解,不会到这
}
// 解法二:一趟扫描(对照实现)
public int partitionDisjoint2(int[] nums) {
int n = nums.length;
int cut = 0;
int curMax = nums[0]; // 当前 left 段的最大值
int maxSeen = nums[0]; // 已扫描全体的最大值
for (int i = 1; i < n; ++i) {
maxSeen = Math.max(maxSeen, nums[i]);
if (nums[i] < curMax) {
// 该元素必须并入 left,left 扩展到 i
cut = i;
curMax = maxSeen;
}
}
return cut + 1;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{5, 0, 3, 8, 6},
{1, 1, 1, 0, 6, 12},
{1, 2, 3, 4, 5},
{5, 4, 3, 2, 1},
{32, 57, 24, 19, 0, 89}
};
// 期望值由 Python 核对:case4 递减序列 left 必须包含全部 5 个;case5=5
int[] exp = {3, 4, 1, 5, 5};
for (int t = 0; t < arrs.length; ++t) {
++total;
int g1 = s.partitionDisjoint(arrs[t]);
int g2 = s.partitionDisjoint2(arrs[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 915-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0915 PASS ==");
} else {
System.out.println("== 0915 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(左右最值) | O(n) | O(n) |
| 解法二(一趟双值) | O(n) | O(1) |
916. Word Subsets(单词子集)
题目描述
给你两个字符串数组 words1 和 words2。
现在,如果 b 中的每个字母都出现在 a 中,包括重复出现的字母,那么称字符串 b 是
字符串 a 的子集。
例如,"wrr" 是 "warrior" 的子集,但不是 "world" 的子集。
如果对 words2 中的每一个单词 b,b 都是 a 的子集,那么我们称 words1 中的单词
a 是通用单词。
以数组形式返回 words1 中所有的通用单词。你可以按任意顺序返回答案。
示例 1
- 输入:words1 = ["amazon","apple","facebook","google","leetcode"], words2 = ["e","o"]
- 输出:["facebook","google","leetcode"]
示例 2
- 输入:words1 = ["amazon","apple","facebook","google","leetcode"], words2 = ["
l","e"] - 输出:["apple","google","leetcode"]
提示: 1 <= words1.length, words2.length <= 10^41 <= words1[i].length, words2[i].length <= 10words1[i]和words2[i]仅由小写英文字母组成words1中的所有字符串互不相同
解题思路
b 是 a 的子集当且仅当对每个字母 c,a 中 c 的出现次数 >= b 中 c 的出现次数。
对多个 b:每个字母统计 words2 中该字母的"最大需求次数",得到一个需求向量 need;
然后遍历 words1,看每个单词的字母频率是否全部 >= need。
解法一(频率向量 + 合并需求,主提交):先合并 words2 的频次需求,再逐词检查 words1。
解法二(按 word2 逐词检查 + 剪枝,对照实现):直接对每个 words1 单词累计 words2
所有单词的频次需求再比较,逻辑相同但实现稍冗余,展示两种写法。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
// 解法一:合并需求向量(推荐提交)
public List<String> wordSubsets(String[] words1, String[] words2) {
int[] need = new int[26];
for (String b : words2) {
int[] f = freq(b);
for (int i = 0; i < 26; ++i) {
need[i] = Math.max(need[i], f[i]);
}
}
List<String> ans = new ArrayList<>();
for (String a : words1) {
int[] f = freq(a);
boolean ok = true;
for (int i = 0; i < 26; ++i) {
if (f[i] < need[i]) {
ok = false;
break;
}
}
if (ok) {
ans.add(a);
}
}
return ans;
}
private int[] freq(String s) {
int[] f = new int[26];
for (char c : s.toCharArray()) {
++f[c - 'a'];
}
return f;
}
// 解法二:逐词检查(对照实现)
public List<String> wordSubsets2(String[] words1, String[] words2) {
List<String> ans = new ArrayList<>();
for (String a : words1) {
int[] fa = freq(a);
boolean ok = true;
for (String b : words2) {
int[] fb = freq(b);
for (int i = 0; i < 26; ++i) {
if (fa[i] < fb[i]) {
ok = false;
break;
}
}
if (!ok) {
break;
}
}
if (ok) {
ans.add(a);
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] w1s = {
{"amazon", "apple", "facebook", "google", "leetcode"},
{"amazon", "apple", "facebook", "google", "leetcode"},
{"aaaa", "aa", "a"},
{"abc", "abd", "abb"}
};
String[][] w2s = {
{"e", "o"},
{"l", "e"},
{"aa"},
{"a", "b", "bb"}
};
String[][] exp = {
{"facebook", "google", "leetcode"},
{"apple", "google", "leetcode"},
{"aaaa", "aa"},
{"abb"}
};
for (int t = 0; t < w1s.length; ++t) {
++total;
java.util.List<String> g1 = s.wordSubsets(w1s[t], w2s[t]);
java.util.List<String> g2 = s.wordSubsets2(w1s[t], w2s[t]);
java.util.Collections.sort(g1);
java.util.Collections.sort(g2);
java.util.List<String> e = new ArrayList<>();
for (String x : exp[t]) {
e.add(x);
}
java.util.Collections.sort(e);
if (g1.equals(e) && g2.equals(e)) {
++pass;
} else {
System.out.println("FAIL 916-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + e);
}
}
if (pass == total) {
System.out.println("== 0916 PASS ==");
} else {
System.out.println("== 0916 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(合并需求) | O((n1 + n2) * L + 26 * n1) | O(26) |
| 解法二(逐词检查) | O(26 * n1 * n2) | O(26) |
917. Reverse Only Letters(仅仅反转字母)
题目描述
给你一个字符串 s,根据下述规则反转字符串:
- 所有非英文字母保留在原有位置。
- 所有英文字母(小写或大写)位置反转。
返回反转后的s。
示例 1
- 输入:
s= "ab-cd" - 输出:"dc-ba"
示例 2
- 输入:
s= "a-bC-dEf-ghIj" - 输出:"
j-Ih-gfE-dCba"
示例 3
- 输入:
s= "Test1ng-Leet=code-Q!" - 输出:"Qedo1ct-
eeLg=ntse-T!"
提示: 1 <= s.length <= 100s仅由 ASCII 值在范围[33, 122]的字符组成s不含'\"'或'\\'
解题思路
双指针从两端向中间,忽略非字母字符,交换遇到的成对字母。
解法一(双指针交换,主提交):l 从头、r 从尾;l 跳过非字母、r 跳过非字母,
当 l < r 时交换两个字符。
解法二(收集字母逆序填充,对照实现):先收集所有字母按原序,再倒序填回原串的字母
槽位,非字母保持不动。
代码实现
class Solution {
// 解法一:双指针交换(推荐提交)
public String reverseOnlyLetters(String s) {
char[] cs = s.toCharArray();
int l = 0, r = cs.length - 1;
while (l < r) {
while (l < r && !Character.isLetter(cs[l])) {
++l;
}
while (l < r && !Character.isLetter(cs[r])) {
--r;
}
if (l < r) {
char t = cs[l];
cs[l] = cs[r];
cs[r] = t;
++l;
--r;
}
}
return new String(cs);
}
// 解法二:收集字母逆序填充(对照实现)
public String reverseOnlyLetters2(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (Character.isLetter(c)) {
sb.append(c);
}
}
String letters = sb.reverse().toString();
char[] cs = s.toCharArray();
int idx = 0;
for (int i = 0; i < cs.length; ++i) {
if (Character.isLetter(cs[i])) {
cs[i] = letters.charAt(idx++);
}
}
return new String(cs);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[] ss = {"ab-cd", "a-bC-dEf-ghIj", "Test1ng-Leet=code-Q!", "a", "---"};
String[] exp = {"dc-ba", "j-Ih-gfE-dCba", "Qedo1ct-eeLg=ntse-T!", "a", "---"};
for (int t = 0; t < ss.length; ++t) {
++total;
String g1 = s.reverseOnlyLetters(ss[t]);
String g2 = s.reverseOnlyLetters2(ss[t]);
if (g1.equals(exp[t]) && g2.equals(exp[t])) {
++pass;
} else {
System.out.println("FAIL 917-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0917 PASS ==");
} else {
System.out.println("== 0917 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(双指针) | O(n) | O(1)(原地,字符串需转字符数组) |
| 解法二(收集逆填) | O(n) | O(n) |
918. Maximum Sum Circular Subarray(环形子数组的最大和)
题目描述
给定一个长度为 n 的环形整数数组 nums,返回 nums 的非空子数组的最大可能和。
环形数组意味着数组的末端将会与开头相连呈环状。形式上,nums[i] 的下一个元素是
nums[(i+1) % n],nums[i] 的前一个元素是 nums[(i-1+n) % n]。
子数组最多只能包含固定缓冲区 nums 中的每个元素一次。
示例 1
- 输入:
nums= [1,-2,3,-2] - 输出:3
- 解释:从子数组 [3] 得到最大和 3。
示例 2
- 输入:
nums= [5,-3,5] - 输出:10
- 解释:从子数组 [5,5] 得到最大和 5 + 5 = 10。
示例 3
- 输入:
nums= [3,-2,2,-3] - 输出:3
- 解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3。
提示: 1 <= nums.length <= 3 * 10^4-3 * 10^4 <= nums[i] <= 3 * 10^4
解题思路
环形最大子段和 = max(普通最大子段和, 总和 - 普通最小子段和)。后者表示跨过首尾的
环上子数组,等价于去掉中间的"补集"(最小子段)。
注意特殊情况:若整个数组全是负数,普通最大子段和为最大负数,而 总和 - 最小子段 会
得到 0(不允许空子数组),所以此时答案就是普通最大子段和。
解法一(一次遍历同时求最大/最小子段和,主提交):Kadane 同时维护 maxEnding 与
minEnding,另附总和非负判断,O(n)。
解法二(双数组前缀 + 单调队列,对照实现):复制数组成两倍长,滑动窗口长度 <= n,
用前缀和 + 单调队列求环上不超过 n 长度的最大子段和,直观但代码更长。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 解法一:Kadane 最大/最小子段和(推荐提交)
public int maxSubarraySumCircular(int[] nums) {
int total = 0;
int curMax = 0, maxSum = Integer.MIN_VALUE;
int curMin = 0, minSum = Integer.MAX_VALUE;
for (int x : nums) {
total += x;
curMax = Math.max(curMax + x, x);
maxSum = Math.max(maxSum, curMax);
curMin = Math.min(curMin + x, x);
minSum = Math.min(minSum, curMin);
}
// 全是负数时,total - minSum == 0(空子数组),不合法,退回 maxSum
if (maxSum < 0) {
return maxSum;
}
return Math.max(maxSum, total - minSum);
}
// 解法二:双倍数组 + 单调队列求长 <= n 的最大子段和(对照实现)
public int maxSubarraySumCircular2(int[] nums) {
int n = nums.length;
int[] a = new int[2 * n];
for (int i = 0; i < 2 * n; ++i) {
a[i] = nums[i % n];
}
// 前缀和
long[] pre = new long[2 * n + 1];
for (int i = 0; i < 2 * n; ++i) {
pre[i + 1] = pre[i] + a[i];
}
// 单调队列维护 pre 的最小值(窗口长度 <= n)
Deque<Integer> q = new ArrayDeque<>();
q.offer(0);
long ans = Long.MIN_VALUE;
for (int i = 1; i <= 2 * n; ++i) {
// 出队:窗口 [i-n+1, i] 维护头元素的合法性
while (!q.isEmpty() && q.peekFirst() < i - n) {
q.pollFirst();
}
ans = Math.max(ans, pre[i] - pre[q.peekFirst()]);
while (!q.isEmpty() && pre[q.peekLast()] >= pre[i]) {
q.pollLast();
}
q.offerLast(i);
}
return (int) ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{1, -2, 3, -2},
{5, -3, 5},
{3, -2, 2, -3},
{1, 2, 3, 4},
{-1, -2, -3},
{0, 5, 8, -9, 9, 10}
};
// case6:总和 23 - 最小子段(-9) = 32(环上跨过 -9 的 [5,8,-9,9,10] 补集),Python 核对
int[] exp = {3, 10, 3, 10, -1, 32};
for (int t = 0; t < arrs.length; ++t) {
++total;
int g1 = s.maxSubarraySumCircular(arrs[t]);
int g2 = s.maxSubarraySumCircular2(arrs[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 918-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0918 PASS ==");
} else {
System.out.println("== 0918 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(Kadane 双极值) | O(n) | O(1) |
| 解法二(前缀和 + 单调队列) | O(n) | O(n) |
919. Complete Binary Tree Inserter(完全二叉树插入器)
题目描述
完全二叉树是每一层(除最后一层外)都是完全填充(即,节点数达到最大)的,并且所有的
节点都尽可能地集中在左侧。
设计一种算法,将一个新节点插入到一棵完全二叉树中,并在插入后保持其完整。
实现 CBTInserter 类:
CBTInserter(TreeNode root)使用头节点为root的给定树初始化该数据结构;CBTInserter.insert(int v)向树中插入一个值为Node.val == v的新节点
TreeNode。使树保持完全二叉树的状态,并返回插入节点TreeNode的父节点的值;CBTInserter.get_root()将返回树的头节点。
示例 1
- 输入:
["CBTInserter", "insert", "insert", "get_root"]
[[[1, 2]], [3], [4], []] - 输出:[
null, 1, 2, [1, 2, 3, 4]] - 解释:
CBTInserter cBTInserter = new CBTInserter([1, 2]);
cBTInserter.insert(3); // 返回 1
cBTInserter.insert(4); // 返回 2
cBTInserter.get_root(); // 返回 [1, 2, 3, 4]
提示: - 树中节点数量范围为
[1, 1000] 0 <= Node.val <= 5000root是完全二叉树insert调用次数不超过10^4
解题思路
完全二叉树按层序(BFS)遍历时,第一个"有空缺"的节点就是下一个插入位置:要么它的
左孩子为空(插左边),要么左孩子非空但右孩子为空(插右边)。
解法一(BFS 预收集候选父节点,主提交):构造时用队列层序遍历所有节点,把拥有至少一个
空孩子(即左右孩子不全)的节点按顺序存进 candidate 队列。insert(v) 弹出队首
候选:左空插左、否则插右;插入后若该节点补满了,则把新节点加入候选队列(新节点本身
没有孩子),并把队首弹掉。逻辑用游标维护,避免频繁出入队。
解法二(每次都 BFS 找候选,对照实现):insert 时重新层序遍历找第一个空位父节点,
直观但每次 O(n),仅作对照。
代码实现
import java.util.ArrayDeque;
import java.util.Queue;
class CBTInserter {
private final TreeNode root;
private final Queue<TreeNode> cand; // 有残缺孩子的节点按序存放
public CBTInserter(TreeNode root) {
this.root = root;
cand = new ArrayDeque<>();
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (node.left == null || node.right == null) {
cand.offer(node);
}
if (node.left != null) {
q.offer(node.left);
}
if (node.right != null) {
q.offer(node.right);
}
}
}
public int insert(int v) {
TreeNode parent = cand.peek();
TreeNode child = new TreeNode(v);
if (parent.left == null) {
parent.left = child;
} else {
parent.right = child;
cand.poll(); // 该节点已满,出队
}
// 新节点没有孩子,是后续潜在的候选
cand.offer(child);
return parent.val;
}
public TreeNode get_root() {
return root;
}
// 解法二:每次 BFS 找空位(对照实现)
static class CBTInserter2 {
private final TreeNode root;
public CBTInserter2(TreeNode root) {
this.root = root;
}
public int insert(int v) {
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (node.left == null) {
node.left = new TreeNode(v);
return node.val;
}
q.offer(node.left);
if (node.right == null) {
node.right = new TreeNode(v);
return node.val;
}
q.offer(node.right);
}
return -1; // 不会到这
}
public TreeNode get_root() {
return root;
}
}
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
// 层序建树
private static TreeNode build(Integer[] vals) {
if (vals == null || vals.length == 0) {
return null;
}
TreeNode root = new TreeNode(vals[0]);
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < vals.length) {
TreeNode cur = q.poll();
if (vals[i] != null) {
cur.left = new TreeNode(vals[i]);
q.offer(cur.left);
}
++i;
if (i < vals.length && vals[i] != null) {
cur.right = new TreeNode(vals[i]);
q.offer(cur.right);
}
++i;
}
return root;
}
private static String levelStr(TreeNode root) {
StringBuilder sb = new StringBuilder("[");
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
boolean anyNonNull = true;
while (!q.isEmpty() && anyNonNull) {
anyNonNull = false;
int sz = q.size();
for (int i = 0; i < sz; ++i) {
TreeNode node = q.poll();
if (node == null) {
sb.append("null,");
continue;
}
sb.append(node.val).append(',');
if (node.left != null || node.right != null) {
anyNonNull = true;
}
q.offer(node.left);
q.offer(node.right);
}
}
String s = sb.toString();
while (s.endsWith("null,")) {
s = s.substring(0, s.length() - 5);
}
return s.substring(0, s.length() - 1) + "]";
}
// ===== 本地测试 =====
public static void main(String[] args) {
int pass = 0, total = 0;
{
CBTInserter c = new CBTInserter(build(new Integer[]{1, 2}));
++total;
if (c.insert(3) == 1 && c.insert(4) == 2
&& levelStr(c.get_root()).equals("[1,2,3,4]")) {
++pass;
} else {
System.out.println("FAIL 919-1: " + levelStr(c.get_root()));
}
}
{
CBTInserter2 c = new CBTInserter2(build(new Integer[]{1, 2, 3, 4, 5, 6}));
++total;
if (c.insert(7) == 3 && c.insert(8) == 4
&& levelStr(c.get_root()).equals("[1,2,3,4,5,6,7,8]")) {
++pass;
} else {
System.out.println("FAIL 919-2: " + levelStr(c.get_root()));
}
}
{
CBTInserter c = new CBTInserter(build(new Integer[]{1}));
++total;
if (c.insert(2) == 1 && levelStr(c.get_root()).equals("[1,2]")) {
++pass;
} else {
System.out.println("FAIL 919-3");
}
}
if (pass == total) {
System.out.println("== 0919 PASS ==");
} else {
System.out.println("== 0919 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(候选队列) | 构造 O(n) / insert O(1) | O(n) |
| 解法二(每次 BFS) | insert O(n) | O(n) |
920. Number of Music Playlists(播放列表的数量)
题目描述
你的音乐播放器里有 n 首不同的歌,在旅途中,你计划听 goal 首歌(不一定不同,即,
允许歌曲重复)。你将会按如下规则创建播放列表:
- 每首歌至少播放一次。
- 一首歌只有在其他
k首歌播放完之后才能再次播放。
给你n、goal和k,返回可以满足要求的播放列表的数量。由于答案可能非常大,请
返回对10^9 + 7取余的结果。
示例 1
- 输入:
n= 3, goal = 3,k= 1 - 输出:6
- 解释:有 6 种可能的播放列表。[1, 2, 3],[1, 3, 2],[2, 1, 3],[2, 3, 1],[3, 1, 2],
[3, 2, 1]。
示例 2
- 输入:
n= 2, goal = 3,k= 0 - 输出:6
- 解释:有 6 种可能的播放列表。[1, 1, 2],[1, 2, 1],[2, 1, 1],[2, 2, 1],[2, 1, 2],
[1, 2, 2]。
示例 3
- 输入:
n= 2, goal = 3,k= 1 - 输出:2
- 解释:有 2 种可能的播放列表。[1, 2, 1],[2, 1, 2]。
提示: 0 <= k < n <= goal <= 100
解题思路
动态规划:dp[i][j] 表示听完 i 首歌(已填充播放列表前 i 个位置)且使用了
j 首不同歌曲的播放列表数量。
转移考虑第 i 个位置放什么歌:
- 放一首"从未用过"的歌:有
n - (j-1)种选择,dp[i-1][j-1] * (n - j + 1); - 放一首"用过"的歌:前
k首不能重复,能用的旧歌为j - k首(当j > k),
贡献dp[i-1][j] * max(0, j - k)。
答案dp[goal][n]。
解法一(二维 DP,主提交):按上述转移 O(goal * n)。
解法二(滚动数组优化,对照实现):状态只依赖上一行,用一维数组滚动;另外附
组合数 + 容斥的对照公式实现,展示不同切入点。
代码实现
class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:二维 DP(推荐提交)
public int numMusicPlaylists(int n, int goal, int k) {
long[][] dp = new long[goal + 1][n + 1];
dp[0][0] = 1;
for (int i = 1; i <= goal; ++i) {
for (int j = 1; j <= n; ++j) {
// 放新歌
long ways = dp[i - 1][j - 1] * (n - j + 1) % MOD;
// 放旧歌(前 k 首不能重,可用旧歌数为 j - k)
if (j > k) {
ways = (ways + dp[i - 1][j] * (j - k)) % MOD;
}
dp[i][j] = ways;
}
}
return (int) dp[goal][n];
}
// 解法二:滚动数组 + 幂次/组合容斥对照(滚动实现)
public int numMusicPlaylists2(int n, int goal, int k) {
long[] dp = new long[n + 1];
dp[0] = 1;
for (int i = 1; i <= goal; ++i) {
long[] ndp = new long[n + 1];
for (int j = 1; j <= n; ++j) {
long ways = ndp[j];
ways = (ways + dp[j - 1] * (n - j + 1)) % MOD;
if (j > k) {
ways = (ways + dp[j] * (j - k)) % MOD;
}
ndp[j] = ways;
}
dp = ndp;
}
return (int) dp[n];
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] cases = {
{3, 3, 1},
{2, 3, 0},
{2, 3, 1},
{3, 4, 1},
{1, 1, 0},
{5, 10, 4}
};
// 期望值由 Python 同款 DP 核对:case4=18,case6=120
int[] exp = {6, 6, 2, 18, 1, 120};
for (int t = 0; t < cases.length; ++t) {
++total;
int g1 = s.numMusicPlaylists(cases[t][0], cases[t][1], cases[t][2]);
int g2 = s.numMusicPlaylists2(cases[t][0], cases[t][1], cases[t][2]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 920-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0920 PASS ==");
} else {
System.out.println("== 0920 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(二维 DP) | O(goal * n) | O(goal * n) |
| 解法二(滚动数组) | O(goal * n) | O(n) |
921. Minimum Add to Make Parentheses Valid(使括号有效的最少添加)
题目描述
只有满足下面几点之一,括号字符串才是有效的:
- 它是一个空字符串,或者
- 它可以被写成
AB(A与B连接),其中A和B都是有效字符串,或者 - 它可以被写作
(A),其中A是有效字符串。
给定一个括号字符串s,在每一次操作中,你都可以在字符串的任何位置插入一个括号。
例如,如果s = "()))",你可以插入一个开始括号为"(()))"或结束括号为"())))"。
返回为使结果字符串s有效而必须添加的最少括号数。
示例 1
- 输入:
s= "())" - 输出:1
示例 2
- 输入:
s= "(((" - 输出:3
提示: 1 <= s.length <= 1000s[i]为'('或')'
解题思路
用计数器维护"未匹配的左括号数":遇到 ( 计数 +1;遇到 ) 若有未匹配的左括号则
匹配掉一个(计数 -1),否则这个右括号无法匹配,需要补一个左括号(答案 +1)。最后
剩余的未匹配左括号数也需要补右括号,计入答案。
解法一(计数贪心,主提交):单次遍历 O(n) 完成。
解法二(栈模拟,对照实现):用栈模拟,遇到 ) 时若栈顶是 ( 则弹出,否则答案 +1;
结束时答案再加上栈中剩余的 ( 数量。
代码实现
class Solution {
// 解法一:计数贪心(推荐提交)
public int minAddToMakeValid(String s) {
int left = 0, ans = 0;
for (char c : s.toCharArray()) {
if (c == '(') {
++left;
} else {
if (left > 0) {
--left;
} else {
++ans;
}
}
}
return ans + left;
}
// 解法二:栈模拟(对照实现)
public int minAddToMakeValid2(String s) {
java.util.Deque<Character> stack = new java.util.ArrayDeque<>();
int ans = 0;
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(c);
} else {
if (!stack.isEmpty() && stack.peek() == '(') {
stack.pop();
} else {
++ans;
}
}
}
return ans + stack.size();
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[] ss = {"())", "(((", "()", "()))((", "((()))", ")))", "()()"};
int[] exp = {1, 3, 0, 4, 0, 3, 0};
for (int t = 0; t < ss.length; ++t) {
++total;
int g1 = s.minAddToMakeValid(ss[t]);
int g2 = s.minAddToMakeValid2(ss[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 921-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0921 PASS ==");
} else {
System.out.println("== 0921 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(计数) | O(n) | O(1) |
| 解法二(栈) | O(n) | O(n) |
922. Sort Array By Parity II(按奇偶排序数组 II)
题目描述
给定一个非负整数数组 nums,nums 中一半整数是奇数,一半整数是偶数。
对数组进行排序,以便当 nums[i] 为奇数时,i 也是奇数;当 nums[i] 为偶数时,
i 也是偶数。
你可以返回任何满足上述条件的数组作为答案。
示例 1
- 输入:
nums= [4,2,5,7] - 输出:[4,5,2,7]
- 解释:[4,7,2,5],[2,5,4,7],[2,7,4,5] 也会被接受。
示例 2
- 输入:
nums= [2,3] - 输出:[2,3]
提示: 2 <= nums.length <= 2 * 10^4nums.length是偶数nums中一半是偶数0 <= nums[i] <= 1000
解题思路
偶数下标 i 应放偶数、奇数下标放奇数。用两个指针分别指向下一个待填的偶数位和奇数位。
解法一(双指针填放,主提交):遍历原数组,偶数放入偶指针位置(步长 2),奇数放入
奇指针位置(步长 2)。O(n) 且一次填好。
解法二(原地交换,对照实现):偶指针 i 从 0 步长 2 扫描,若 nums[i] 是奇数,
则找奇数指针 j(从 1 步长 2)处为偶数的位置与之交换。原地完成,不使用额外数组。
代码实现
import java.util.Arrays;
class Solution {
// 解法一:双指针填放(推荐提交)
public int[] sortArrayByParityII(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
int even = 0, odd = 1;
for (int x : nums) {
if (x % 2 == 0) {
ans[even] = x;
even += 2;
} else {
ans[odd] = x;
odd += 2;
}
}
return ans;
}
// 解法二:原地交换(对照实现)
public int[] sortArrayByParityII2(int[] nums) {
int n = nums.length;
int j = 1;
for (int i = 0; i < n; i += 2) {
if (nums[i] % 2 != 0) {
// 找奇数下标处是偶数的位置
while (j < n && nums[j] % 2 == 1) {
j += 2;
}
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
return nums;
}
private static boolean isValid(int[] arr) {
for (int i = 0; i < arr.length; ++i) {
if (arr[i] % 2 != i % 2) {
return false;
}
}
return true;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{4, 2, 5, 7},
{2, 3},
{1, 2, 3, 4},
{0, 1, 2, 3, 4, 5},
{2, 0, 3, 1}
};
for (int t = 0; t < arrs.length; ++t) {
++total;
if (isValid(s.sortArrayByParityII(arrs[t].clone()))
&& isValid(s.sortArrayByParityII2(arrs[t].clone()))) {
++pass;
} else {
System.out.println("FAIL 922-" + (t + 1));
}
}
if (pass == total) {
System.out.println("== 0922 PASS ==");
} else {
System.out.println("== 0922 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(双指针填放) | O(n) | O(n) |
| 解法二(原地交换) | O(n) | O(1) |
923. 3Sum With Multiplicity(三数之和的多种可能)
题目描述
给定一个整数数组 arr,以及一个整数 target 作为目标值,返回满足 i < j < k 且
arr[i] + arr[j] + arr[k] == target 的元组 i, j, k 的数量。
由于结果会非常大,请返回 10^9 + 7 的模。
示例 1
- 输入:
arr= [1,1,2,2,3,3,4,4,5,5],target= 8 - 输出:20
- 解释:
按值枚举 (arr[i],arr[j],arr[k]):
(1, 2, 5) 出现 8 次;(1, 3, 4) 出现 8 次;(2, 2, 4) 出现 2 次;(2, 3, 3) 出现 2 次。
示例 2
- 输入:
arr= [1,1,2,2,2,2],target= 5 - 输出:12
- 解释:
arr[i]= 1,arr[j]=arr[k]= 2 出现 12 次:
我们从 [1,1] 中选择一个 1,有 2 种情况,
从 [2,2,2,2] 中选出两个 2,有 6 种情况。
提示: 3 <= arr.length <= 30000 <= arr[i] <= 1000 <= target <= 300
解题思路
元素值范围很小(0..100),可以用计数 + 两两组合统计,或排序 + 双指针。
解法一(计数 + 枚举两数,主提交):统计每个值的出现次数 cnt。枚举 i < j
(值层面),need = target - i - j。若 0 <= need <= 100,按三数取值情况组合计数:
三个都相等、两个相等、全不同分别用组合数公式。注意 i <= j <= need 去重。
解法二(排序 + 双指针,对照实现):排序后经典三数之和双指针,累加出现组合数
lCnt * rCnt 处理重复值,取模返回。
代码实现
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:计数 + 组合数(推荐提交)
public int threeSumMulti(int[] arr, int target) {
long[] cnt = new long[101];
for (int x : arr) {
++cnt[x];
}
long ans = 0;
for (int i = 0; i <= 100; ++i) {
if (cnt[i] == 0) {
continue;
}
for (int j = i; j <= 100; ++j) {
if (cnt[j] == 0) {
continue;
}
int need = target - i - j;
if (need < j || need > 100 || cnt[need] == 0) {
continue;
}
if (i == j && j == need) {
// 三个相同:C(cnt, 3)
ans += comb3(cnt[i]);
} else if (i == j) {
// i==j!=need:C(cnt[i],2) * cnt[need]
ans += comb2(cnt[i]) * cnt[need];
} else if (j == need) {
// i!=j==need:cnt[i] * C(cnt[j],2)
ans += cnt[i] * comb2(cnt[j]);
} else {
// 全不同
ans += cnt[i] * cnt[j] * cnt[need];
}
ans %= MOD;
}
}
return (int) (ans % MOD);
}
private long comb2(long c) {
return c * (c - 1) / 2;
}
private long comb3(long c) {
return c * (c - 1) * (c - 2) / 6;
}
// 解法二:排序 + 双指针(对照实现)
public int threeSumMulti2(int[] arr, int target) {
Arrays.sort(arr);
int n = arr.length;
long ans = 0;
for (int i = 0; i < n - 2; ++i) {
int l = i + 1, r = n - 1;
int need = target - arr[i];
while (l < r) {
int sum = arr[l] + arr[r];
if (sum < need) {
++l;
} else if (sum > need) {
--r;
} else {
if (arr[l] == arr[r]) {
// l..r 段全部相同:C(len,2)
long len = r - l + 1;
ans = (ans + len * (len - 1) / 2) % MOD;
break;
} else {
// 统计左侧重复数、右侧重复数
int lv = arr[l], rv = arr[r];
long lc = 0, rc = 0;
while (l <= r && arr[l] == lv) {
++lc;
++l;
}
while (l <= r && arr[r] == rv) {
++rc;
--r;
}
ans = (ans + lc * rc) % MOD;
}
}
}
}
return (int) ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{1, 1, 2, 2, 3, 3, 4, 4, 5, 5},
{1, 1, 2, 2, 2, 2},
{1, 2, 3},
{0, 0, 0},
{2, 2, 2, 2, 2},
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
};
int[] targets = {8, 5, 6, 0, 6, 12};
// 期望值由 Python 核对:case6=(1,2,9)(1,3,8)(1,4,7)(1,5,6)(2,3,7)(2,4,6)(3,4,5) 共 7
int[] exp = {20, 12, 1, 1, 10, 7};
for (int t = 0; t < arrs.length; ++t) {
++total;
int g1 = s.threeSumMulti(arrs[t], targets[t]);
int g2 = s.threeSumMulti2(arrs[t], targets[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 923-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0923 PASS ==");
} else {
System.out.println("== 0923 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(计数枚举) | O(100^2) | O(100) |
| 解法二(双指针) | O(n^2) | O(log n)(排序栈) |
924. Minimize Malware Spread(尽量减少恶意软件的传播)
题目描述
给出了一个由 n 个节点组成的网络,用 n x n 个邻接矩阵图 graph 表示。在节点网络中,
当 graph[i][j] = 1 时,表示节点 i 能够直接连接到另一个节点 j。
一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到
恶意软件的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多
的节点可以被这种方式感染。
假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。
如果从 initial 中移除某一节点能够最小化 M(initial),返回该节点。如果有多个节点满足
条件,就返回索引最小的节点。
请注意,如果某个节点已从受感染节点的列表 initial 中删除,它以后仍有可能因恶意软件传播
而受到感染。
示例 1
- 输入:
graph= [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1] - 输出:0
示例 2
- 输入:
graph= [[1,0,0],[0,1,0],[0,0,1]], initial = [0,2] - 输出:0
示例 3
- 输入:
graph= [[1,1,1],[1,1,1],[1,1,1]], initial = [1,2] - 输出:1
提示: 1 <= graph.length <= 3000 <= graph[i][j] == graph[j][i] <= 1graph[i][i] == 11 <= initial.length <= graph.length0 <= initial[i] < graph.lengthinitial中所有整数均不重复
解题思路
按连通分量分析:一个连通块若有多个 initial 中的节点,删除其中任意一个,该块仍会被其他
感染节点传播感染,收益为 0;若恰好只有 1 个 initial 节点,删除它能让整个块免于感染,
收益 = 块大小。
解法一(DFS 染色分量 + 统计,主提交):DFS/BFS 给每个连通分量编号,记录分量大小、
分量内的 initial 节点个数。对每个 initial 节点,若它所在块恰好只含它一个感染源,
潜在收益为块大小,取最大收益、同收益取最小索引;若所有块收益都为 0,返回 initial 中
最小的元素。
解法二(并查集,对照实现):用并查集合并连通块,再统计每个根的分量大小与 initial
计数,逻辑与解一同源,展示并查集实现。
代码实现
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:DFS 连通分量分析(推荐提交)
public int minMalwareSpread(int[][] graph, int[] initial) {
int n = graph.length;
int[] comp = new int[n];
Arrays.fill(comp, -1);
int compCount = 0;
for (int i = 0; i < n; ++i) {
if (comp[i] == -1) {
dfs(graph, comp, i, compCount++);
}
}
// 每个分量的节点数
int[] size = new int[compCount];
for (int i = 0; i < n; ++i) {
++size[comp[i]];
}
// 每个分量内 initial 的节点数
int[] infect = new int[compCount];
Set<Integer> initSet = new HashSet<>();
for (int v : initial) {
initSet.add(v);
++infect[comp[v]];
}
int best = initial[0];
int bestGain = -1;
for (int v : initial) {
int c = comp[v];
if (infect[c] == 1) {
// 独立感染源:收益就是块大小
if (size[c] > bestGain || (size[c] == bestGain && v < best)) {
best = v;
bestGain = size[c];
}
} else {
// 同块多感染源:删除无收益,但作为兜底候选
if (bestGain == -1 && v < best) {
best = v;
}
}
}
return best;
}
private void dfs(int[][] graph, int[] comp, int u, int id) {
comp[u] = id;
for (int v = 0; v < graph.length; ++v) {
if (graph[u][v] == 1 && comp[v] == -1) {
dfs(graph, comp, v, id);
}
}
}
// 解法二:并查集(对照实现)
public int minMalwareSpread2(int[][] graph, int[] initial) {
int n = graph.length;
int[] parent = new int[n];
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (graph[i][j] == 1) {
union(parent, i, j);
}
}
}
int[] size = new int[n];
for (int i = 0; i < n; ++i) {
++size[find(parent, i)];
}
int[] infect = new int[n];
Set<Integer> initSet = new HashSet<>();
for (int v : initial) {
initSet.add(v);
++infect[find(parent, v)];
}
int best = initial[0];
int bestGain = -1;
for (int v : initial) {
int root = find(parent, v);
if (infect[root] == 1) {
if (size[root] > bestGain || (size[root] == bestGain && v < best)) {
best = v;
bestGain = size[root];
}
} else {
if (bestGain == -1 && v < best) {
best = v;
}
}
}
return best;
}
private int find(int[] parent, int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
private void union(int[] parent, int a, int b) {
int ra = find(parent, a), rb = find(parent, b);
if (ra != rb) {
parent[ra] = rb;
}
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] graphs = {
{{1, 1, 0}, {1, 1, 0}, {0, 0, 1}},
{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}},
{{1, 1, 1}, {1, 1, 1}, {1, 1, 1}},
{{1, 0, 1, 0}, {0, 1, 0, 0}, {1, 0, 1, 1}, {0, 0, 1, 1}}
};
int[][] initials = {
{0, 1},
{0, 2},
{1, 2},
{0, 2}
};
// case4:{0,2,3} 同块两个感染源,删除无收益,兜底返回最小索引 0(Python 核对)
int[] exp = {0, 0, 1, 0};
for (int t = 0; t < graphs.length; ++t) {
++total;
int g1 = s.minMalwareSpread(graphs[t], initials[t]);
int g2 = s.minMalwareSpread2(graphs[t], initials[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 924-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0924 PASS ==");
} else {
System.out.println("== 0924 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(DFS 染色) | O(n^2) | O(n) |
| 解法二(并查集) | O(n^2 * α) | O(n) |
925. Long Pressed Name(长按键入)
题目描述
你的朋友正在使用键盘输入他的名字 name。偶尔,在键入字符 c 时,按键可能会被长按,
而字符可能被输入 1 次或多次。
你将会检查键盘输入的字符 typed。如果它对应的可能是你的朋友的名字(其中一些字符可能
被长按),那么就返回 True。
示例 1
- 输入:name = "alex", typed = "aaleex"
- 输出:
true - 解释:'alex' 中的 '
a' 和 'e' 被长按。
示例 2
- 输入:name = "saeed", typed = "ssaaedd"
- 输出:
false - 解释:'e' 一定需要被键入两次,但在 typed 的输出中不是这样。
示例 3
- 输入:name = "leelee", typed = "lleeelee"
- 输出:
true
示例 4
- 输入:name = "laiden", typed = "laiden"
- 输出:
true
提示: 1 <= name.length, typed.length <= 1000name和typed的字符都是小写字母
解题思路
双指针扫描:i 指向 name、j 指向 typed。
- 若
name[i] == typed[j],双双前进; - 否则若
typed[j] == typed[j-1](长按多打的字符),j前进; - 否则直接失败。
结束条件:name走完且typed剩余字符全是最后一个字符的重复。
解法一(双指针,主提交):按上述规则 O(len(name) + len(typed))。
解法二(分组计数,对照实现):把两个串各自按连续相同字符分组为 (char, count) 列表,
逐一比较:字符必须一致且 name 组内长度 <= typed 组内长度。直观但代码稍长。
代码实现
class Solution {
// 解法一:双指针(推荐提交)
public boolean isLongPressedName(String name, String typed) {
int i = 0, j = 0;
int m = name.length(), n = typed.length();
while (j < n) {
if (i < m && name.charAt(i) == typed.charAt(j)) {
++i;
++j;
} else if (j > 0 && typed.charAt(j) == typed.charAt(j - 1)) {
++j; // 长按重复字符
} else {
return false;
}
}
return i == m;
}
// 解法二:分组计数(对照实现)
public boolean isLongPressedName2(String name, String typed) {
java.util.List<int[]> g1 = group(name);
java.util.List<int[]> g2 = group(typed);
if (g1.size() != g2.size()) {
return false;
}
for (int k = 0; k < g1.size(); ++k) {
if (g1.get(k)[0] != g2.get(k)[0] || g1.get(k)[1] > g2.get(k)[1]) {
return false;
}
}
return true;
}
private java.util.List<int[]> group(String s) {
java.util.List<int[]> res = new java.util.ArrayList<>();
int i = 0;
while (i < s.length()) {
int j = i;
while (j < s.length() && s.charAt(j) == s.charAt(i)) {
++j;
}
res.add(new int[]{s.charAt(i), j - i});
i = j;
}
return res;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[] names = {"alex", "saeed", "leelee", "laiden", "abc", "xnhtq"};
String[] typeds = {"aaleex", "ssaaedd", "lleeelee", "laiden", "aabbcc", "xhhttqq"};
boolean[] exp = {true, false, true, true, true, false};
for (int t = 0; t < names.length; ++t) {
++total;
boolean g1 = s.isLongPressedName(names[t], typeds[t]);
boolean g2 = s.isLongPressedName2(names[t], typeds[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 925-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0925 PASS ==");
} else {
System.out.println("== 0925 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(双指针) | O(len(name) + len(typed)) | O(1) |
| 解法二(分组计数) | O(len(name) + len(typed)) | O(len) |