LeetCode 276-300 题解合集
文章目录
- [LeetCode 276-300 题解合集](#LeetCode 276-300 题解合集)
-
- [276. Paint Fence(栅栏涂色)](#276. Paint Fence(栅栏涂色))
- [277. Find the Celebrity(搜寻名人)](#277. Find the Celebrity(搜寻名人))
- [278. First Bad Version(第一个错误的版本)](#278. First Bad Version(第一个错误的版本))
- [279. Perfect Squares(完全平方数)](#279. Perfect Squares(完全平方数))
- [280. Wiggle Sort(摆动排序)](#280. Wiggle Sort(摆动排序))
- [281. Zigzag Iterator(锯齿迭代器)](#281. Zigzag Iterator(锯齿迭代器))
- [282. Expression Add Operators(给表达式添加运算符)](#282. Expression Add Operators(给表达式添加运算符))
- [283. Move Zeroes(移动零)](#283. Move Zeroes(移动零))
- [284. Peeking Iterator(窥视迭代器)](#284. Peeking Iterator(窥视迭代器))
- [285. Inorder Successor in BST(二叉搜索树中的中序后继)](#285. Inorder Successor in BST(二叉搜索树中的中序后继))
- [286. Walls and Gates(墙与门)](#286. Walls and Gates(墙与门))
- [287. Find the Duplicate Number(寻找重复数)](#287. Find the Duplicate Number(寻找重复数))
- [288. Unique Word Abbreviation(单词的唯一缩写)](#288. Unique Word Abbreviation(单词的唯一缩写))
- [289. Game of Life(生命游戏)](#289. Game of Life(生命游戏))
- [290. Word Pattern(单词规律)](#290. Word Pattern(单词规律))
- [291. Word Pattern II(单词规律 II)](#291. Word Pattern II(单词规律 II))
- [292. Nim Game(Nim 游戏)](#292. Nim Game(Nim 游戏))
- [293. Flip Game(翻转游戏)](#293. Flip Game(翻转游戏))
- [294. Flip Game II(翻转游戏 II)](#294. Flip Game II(翻转游戏 II))
- [295. Find Median from Data Stream(数据流的中位数)](#295. Find Median from Data Stream(数据流的中位数))
- [296. Best Meeting Point(最佳的碰头地点)](#296. Best Meeting Point(最佳的碰头地点))
- [297. Serialize and Deserialize Binary Tree(二叉树的序列化与反序列化)](#297. Serialize and Deserialize Binary Tree(二叉树的序列化与反序列化))
- [298. Binary Tree Longest Consecutive Sequence(二叉树最长连续序列)](#298. Binary Tree Longest Consecutive Sequence(二叉树最长连续序列))
- [299. Bulls and Cows(猜数字游戏)](#299. Bulls and Cows(猜数字游戏))
- [300. Longest Increasing Subsequence(最长递增子序列)](#300. Longest Increasing Subsequence(最长递增子序列))
276. Paint Fence(栅栏涂色)
题目描述
有 k 种颜色的涂料和一个包含 n 个栅栏柱的栅栏,请你按下述规则为栅栏设计涂色方案:
每个栅栏柱可以用其中一种颜色进行上色;相邻的栅栏柱最多连续两个颜色相同。
给你两个整数 k 和 n,返回所有有效的涂色方案数。
- 输入 :
intn,intk(1 <=n<= 50,1 <=k<= 10^5,k* (k- 1) 可能超过int范围) - 输出 :
int(方案总数;结果会落在int范围内)
示例 1:
n = 3, k = 2 -> 6(前 3 个柱用 2 种颜色的全部合法方案;
全涂红或全涂绿除外,因为会连续 3 个同色)
示例 2:
n = 1, k = 1 -> 1
示例 3:
n = 2, k = 3 -> 9(第一柱 3 色任意,第二柱 3 色任意,相邻两列相同也允许(最多连续两个))
约束:
- 1 <=
n<= 50;1 <=k<= 10^5。
解题思路
DP 分"与前一柱同色/异色"两状态 / 数学递推合并
- 解法一(两状态 DP):用 same(最后两根同色的方案数)与 diff(最后两根异色的方案数)滚动。
初始化:第一根柱子任意 k 色,same 无前驱取 0(一根柱子谈不上"两根同色"),diff = k。
每加一根柱子:
新 same = 旧 diff × 1(要同色,只能与上一根相同颜色,故乘 1,且上一根必须与再上一根异色)
新 diff = (旧 same + 旧 diff) × (k - 1)(要与上一根异色,可选 k-1 种,不关心上一根同色与否)
最终方案数 = same + diff。 - 解法二(压缩成单一递推):设 fn 为 n 根柱子的方案总数,直接有
fn = (k - 1) * (fn-1 + fn-2),边界 f1 = k、f2 = kk。
该式与两状态等价(fn 中"与最后一根同色"的有 fn-2 (k-1) 种、"异色"的有 fn-1*(k-1) 种),
代码更短,但数值增长快,结果也应使用 long 中间量再转 int。
为什么"最多连续两个相同"只约束最后两柱:只要保证新增柱子不与前一柱同色,或与前一根同色时
必须要求"倒数第三根与倒数第二根异色",那么任意更早的连续段长度都不会超过 2;该局部约束
恰好捕获了全部非法构型。
代码实现
java
public class Solution {
public int numWays(int n, int k) {
if (n == 0 || k == 0) {
return 0;
}
long same = 0; // 最后两根同色
long diff = k; // 最后一根柱子任意色(作为"与前一不同色"的起始:单根柱定义 diff=k)
for (int i = 2; i <= n; i++) {
long newSame = diff; // 与上一根同色:只能取上一根颜色
long newDiff = (same + diff) * (k - 1); // 与上一根异色:k-1 种颜色可选
same = newSame;
diff = newDiff;
}
return (int) (same + diff);
}
public int numWaysFormula(int n, int k) {
if (n == 0 || k == 0) {
return 0;
}
if (n == 1) {
return k;
}
long f1 = k;
long f2 = (long) k * k; // n=2:两柱任意涂,允许同色
for (int i = 3; i <= n; i++) {
long f3 = (k - 1) * (f1 + f2);
f1 = f2;
f2 = f3;
}
return (int) f2;
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.numWays(3, 2) == 6, "示例1: n=3,k=2 -> 6");
check(s.numWays(1, 1) == 1, "示例2: n=1,k=1 -> 1");
check(s.numWays(2, 3) == 9, "示例3: n=2,k=3 -> 9");
check(s.numWays(1, 5) == 5, "n=1,k=5 -> 5");
check(s.numWays(2, 1) == 1, "n=2,k=1 -> 1(两柱只能同色,恰好合法)");
check(s.numWays(3, 1) == 0, "n=3,k=1 -> 0(三柱必连续同色)");
check(s.numWays(7, 2) == 42, "n=7,k=2 -> 42");
check(s.numWays(50, 100000) >= 0, "n=50,k=100000 不溢出");
check(s.numWaysFormula(3, 2) == 6, "公式: n=3,k=2 -> 6");
check(s.numWaysFormula(2, 3) == 9, "公式: n=2,k=3 -> 9");
check(s.numWaysFormula(1, 1) == 1, "公式: n=1,k=1 -> 1");
// 两种解法对拍
for (int n = 1; n <= 12; n++) {
for (int k = 1; k <= 8; k++) {
if (s.numWays(n, k) != s.numWaysFormula(n, k)) {
check(false, "对拍不一致 n=" + n + " k=" + k);
}
}
}
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(1)(滚动两个 long);
解法二:时间 O(n),空间 O(1)。
277. Find the Celebrity(搜寻名人)
题目描述
假设你是一个专业的狗仔,参加了一个 n 人派对,其中每个人被从 0 到 n - 1 标号。
在这个派对人群当中可能存在一位"名人"。所谓"名人"的定义是:其他所有 n - 1 个人都认识他/她,
而他/她并不认识其他任何人。现在你想要确认这个"名人"是谁,或者确定这里没有"名人"。
而你唯一能做的就是问诸如"A 你好呀,请问你认不认识 B 呀?"的问题,以确定 A 是否认识 B。
你需要在(渐近意义上)尽可能少的问题内来确定这位"名人"是谁(或者确定这里没有"名人")。
给定整数 n 和一个辅助函数 boolean knows(a, b) 用来获取 a 是否认识 b。
实现一个函数 int findCelebrity(n)。派对最多只会有一位"名人"参加。
若"名人"存在,请返回他/她的编号;若"名人"不存在,请返回 -1。
注意:n x n 的二维数组 graph 给定的输入并不是直接提供给你的,而是只能通过辅助函数 knows 获取。
graph[i][j] == 1 表示 i 认识 j,graph[i][j] == 0 表示 i 不认识 j。
- 输入 :
intn(2 <=n<= 100,graph[i][j]为 0 或 1,graph[i][i]== 1)+ 只能通过 knows 查询的图 - 输出 :
int(名人编号;不存在返回 -1)
示例 1:
graph = [[1,1,0],[0,1,0],[1,1,1]] -> 1(0、2 都认识 1,而 1 不认识任何人)
示例 2:
graph = [[1,0,1],[1,1,0],[0,1,1]] -> -1(没有人满足被所有人认识且不认识任何人)
约束:
n==graph.length==graph[i].length;2 <=n<= 100;graph[i][j]是 0 或 1;
graph[i][i]== 1;最多只有一位名人。进阶:尽可能少调用 knows(不超过 3n次)。
解题思路
两遍扫描排除候选人 / 计数验证法
- 解法一(经典两遍扫描,O(n) 次调用):
第一次扫描:设候选人 cand = 0,从 i = 1 到 n-1,若 knows(cand, i) 为 true 则说明 cand 认识 i,
不是名人,把候选人更新为 i。该扫描利用"名人不认识任何人"剪枝,最多 n-1 次 knows 调用,
最终 cand 是唯一可能是名人的候选人(若存在名人,他永远不会被替换掉)。
第二次验证:对 cand 检查 (a) 是否所有人都认识他:!knows(i, cand) 则失败;
(b) 他是否不认识所有人:knows(cand, i) 为 true(且 i != cand)则失败。验证需要 2n-2 次调用。 - 解法二(直接计数验证):对每个候选 i 与 j(j != i)统计"认识 j 的人数"与"j 认识的人数",
一个节点是名人的充要条件是:认识它的节点数 == n-1 且它认识的节点数 == 0。O(n^2) 次调用,
思路朴素适合对照。进阶要求不超过 3n 次调用,因此主推解法一。
为什么第一遍扫描不会漏掉名人:若存在名人 c,扫到 c 时 knows(c, i) 恒为 false,
因此候选指针永远不会从 c 跳走;其它普通人的替换也只会在非名人之间发生,最终 cand 必为 c。
代码实现
java
public class Solution {
public int findCelebrity(int n) {
int cand = 0;
// 第一遍:排除法找唯一候选人
for (int i = 1; i < n; i++) {
if (knows(cand, i)) {
cand = i; // cand 认识 i,则 cand 不可能是名人;i 成为新的候选
}
}
// 第二遍:验证候选人的"名人"条件
for (int i = 0; i < n; i++) {
if (i == cand) {
continue;
}
if (knows(cand, i) || !knows(i, cand)) {
return -1; // 名人不能认识任何人;且所有人都必须认识名人
}
}
return cand;
}
private static int[][] GRAPH;
public boolean knows(int a, int b) {
return GRAPH[a][b] == 1;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例1: [[1,1,0],[0,1,0],[1,1,1]] -> 1
setGraph(new int[][]{{1, 1, 0}, {0, 1, 0}, {1, 1, 1}});
check(s.findCelebrity(3) == 1, "示例1 -> 1");
// 示例2: [[1,0,1],[1,1,0],[0,1,1]] -> -1
setGraph(new int[][]{{1, 0, 1}, {1, 1, 0}, {0, 1, 1}});
check(s.findCelebrity(3) == -1, "示例2 -> -1");
// 无名人链式:2 人互不认识
setGraph(new int[][]{{1, 0}, {0, 1}});
check(s.findCelebrity(2) == -1, "互不认识 -> -1");
// 2 人,1 号是名人(0 认识 1,1 不认识 0)
setGraph(new int[][]{{1, 1}, {0, 1}});
check(s.findCelebrity(2) == 1, "二人名人 -> 1");
// 4 人,3 号是名人
setGraph(new int[][]{
{1, 1, 1, 1},
{1, 1, 0, 1},
{1, 1, 1, 1},
{0, 0, 0, 1}});
check(s.findCelebrity(4) == 3, "四人名人 -> 3");
System.out.println("全部测试通过");
}
private static void setGraph(int[][] g) {
GRAPH = g;
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n)(约 3n 次 knows 调用),空间 O(1);
解法二:时间 O(n^2) 次调用,空间 O(1)。
278. First Bad Version(第一个错误的版本)
题目描述
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过
质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 1, 2, ..., `n`,你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 boolean isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。
实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
- 输入 :
intn,intbad(1 <= bad <=n<= 2^31 - 1) - 输出 :
int(第一个错误的版本号)
示例 1:
n = 5, bad = 4 -> 4(isBadVersion(3)=false, isBadVersion(5)=true, isBadVersion(4)=true)
示例 2:
n = 1, bad = 1 -> 1
约束:
- 1 <= bad <=
n<= 2^31 - 1;第一个坏版本之后的所有版本都是坏的。
进阶:只调用O(log n)次isBadVersion。
解题思路
二分查找下界 / 线性扫描
- 解法一(二分查找第一个 true):坏版本序列是 good...good, bad...bad 的前假后真模式,
问题转化为"找到第一个 isBadVersion 为 true 的下标"。用 lo=1, hi=n,
mid = lo + (hi - lo) / 2 避免 lo+hi 溢出(n 最大 2^31-1);若 isBadVersion(mid) 为 true,
说明第一个坏版本 <= mid,收 hi = mid;否则 lo = mid + 1。最后 lo 即答案。
每轮调用 1 次 API,共 O(log n) 次。 - 解法二(线性扫描):从 1 开始逐个检查,第一个 true 就是答案;直观但最坏 O(n) 次调用,
不满足进阶要求,仅作对照思路。
为什么二分成立:题目保证"错误的版本之后的所有版本都是错的",即单调性(前 good 后 bad),
这是二分可行性的前提;找"第一个 bad"就是标准的下界二分(lower_bound)。
代码实现
java
public class Solution {
public int firstBadVersion(int n) {
int lo = 1, hi = n;
while (lo < hi) {
int mid = lo + (hi - lo) / 2; // 避免 lo+hi 溢出
if (isBadVersion(mid)) {
hi = mid; // 第一个坏版本在 [lo, mid]
} else {
lo = mid + 1; // mid 是好版本,答案在 (mid, hi]
}
}
return lo;
}
private static int BAD;
private static int CALLS;
public boolean isBadVersion(int version) {
CALLS++;
return version >= BAD;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例1: n=5, bad=4 -> 4
runCase(s, 5, 4, "示例1");
// 示例2: n=1, bad=1 -> 1
runCase(s, 1, 1, "示例2");
// 边界: bad=1
runCase(s, 10, 1, "bad=1 首版本即坏");
// 边界: bad=n
runCase(s, 10, 10, "bad=n 末版本才坏");
// 大数: n 接近 int 上限
runCase(s, 2147483647, 1, "大 n,bad=1");
s.CALLS = 0;
s.BAD = 2147483647;
int r = s.firstBadVersion(2147483647);
check(r == 2147483647, "大 n,bad=n -> " + r);
System.out.println("调用次数=" + s.CALLS + "(O(log n) 验证)");
System.out.println("全部测试通过");
}
private static void runCase(Solution s, int n, int bad, String name) {
s.BAD = bad;
s.CALLS = 0;
int r = s.firstBadVersion(n);
check(r == bad, name + " n=" + n + " bad=" + bad + " -> " + r);
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(log n) 次 API 调用(每次 O(1)),空间 O(1);
解法二:时间 O(n) 次调用,空间 O(1)。
279. Perfect Squares(完全平方数)
题目描述
给你一个整数 n,返回和为 n 的完全平方数的最少数量。
完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。
例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
- 输入 :
intn(1 <=n<= 10^4) - 输出 :
int(和为n的完全平方数的最少数量)
示例 1:
n = 12 -> 3(12 = 4 + 4 + 4)
示例 2:
n = 13 -> 2(13 = 4 + 9)
约束:
- 1 <=
n<= 10^4。
解题思路
动态规划(完全背包) / BFS 最短路 / 四平方和定理
- 解法一(DP/完全背包):dpi = 最少需要多少个完全平方数凑成 i。
初始 dp0=0,其余为 +∞;转移:对所有平方数 jj (j j <= i),
dpi = min(dpi, dpi - j*j + 1)。从小到大枚举 i 与 j,O(n*sqrt(n))。 - 解法二(BFS 最短路):把"数 i"看成节点,从 i 连向 i - jj(j j <= i);求 n 到 0 的最短路径。
每层表示"再减一个完全平方数",首次到达 0 的层数即最少个数。用队列 BFS 保证最短;
空间 O(n)(visited)。 - 解法三(拉格朗日四平方和定理,仅提及):任意正整数可表示为 <= 4 个平方数之和,
且 n = 4^a*(8b+7) 时恰为 4 个。此题 O(1) 可解,但工程实现容易出错,一般面试讲思路即可。
为什么 dp 递推是在做"完全背包":完全平方数可重复选取且不限制次数,dpi 依赖 dpi - j\*j,
是标准的"无限物品、容量 i"的背包模型;从小 i 到大 i 迭代保证每个物品可无限取。
代码实现
java
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;
public class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
public int numSquaresBfs(int n) {
Queue<Integer> q = new ArrayDeque<>();
boolean[] visited = new boolean[n + 1];
q.offer(n);
visited[n] = true;
int level = 0;
while (!q.isEmpty()) {
int size = q.size();
for (int t = 0; t < size; t++) {
int cur = q.poll();
if (cur == 0) {
return level;
}
for (int j = 1; j * j <= cur; j++) {
int next = cur - j * j;
if (!visited[next]) {
visited[next] = true;
q.offer(next);
}
}
}
level++;
}
return level;
}
public int numSquaresTheorem(int n) {
// 缩小 4 的因子:4^a*(8b+7) 是"恰好 4 个"的判定式
int x = n;
while (x % 4 == 0) {
x /= 4;
}
if (x % 8 == 7) {
return 4;
}
// 是否是两平方和
for (int a = 0; a * a <= n; a++) {
int b2 = n - a * a;
int b = (int) Math.sqrt(b2);
if (b * b == b2) {
// 两个平方数之和;若其中一个为 0 则是 1 个
return (a == 0 || b2 == 0) ? 1 : 2;
}
}
return 3;
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.numSquares(12) == 3, "示例1: n=12 -> 3");
check(s.numSquares(13) == 2, "示例2: n=13 -> 2");
check(s.numSquares(1) == 1, "n=1 -> 1");
check(s.numSquares(2) == 2, "n=2 -> 2(1+1)");
check(s.numSquares(4) == 1, "n=4 -> 1");
check(s.numSquares(9) == 1, "n=9 -> 1");
check(s.numSquares(7) == 4, "n=7 -> 4");
check(s.numSquares(48) == 3, "n=48 -> 4*12=3");
check(s.numSquares(10000) == 1, "n=10000=100^2 -> 1");
check(s.numSquaresBfs(12) == 3, "BFS: n=12 -> 3");
check(s.numSquaresBfs(13) == 2, "BFS: n=13 -> 2");
check(s.numSquaresBfs(10000) == 1, "BFS: n=10000 -> 1");
check(s.numSquaresTheorem(12) == 3, "定理: n=12 -> 3");
check(s.numSquaresTheorem(13) == 2, "定理: n=13 -> 2");
check(s.numSquaresTheorem(7) == 4, "定理: n=7 -> 4");
// 对拍三解法(n<=200)
for (int i = 1; i <= 200; i++) {
int a = s.numSquares(i);
int b = s.numSquaresBfs(i);
int c = s.numSquaresTheorem(i);
if (a != b || a != c) {
check(false, "对拍不一致 n=" + i + " dp=" + a + " bfs=" + b + " thm=" + c);
}
}
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(nsqrt(n)),空间 O(n)(dp 数组);
解法二:时间 O(nsqrt(n)),空间 O(n)(visited)。
280. Wiggle Sort(摆动排序)
题目描述
给你一个的整数数组 nums,将该数组重新排序后使 nums[0] <= nums[1] >= nums[2] <= nums[3]...
输入数组总是有一个有效的答案。
- 输入 :
int[]nums(1 <=nums.length<= 5 * 10^4,0 <=nums[i]<= 10^4) - 输出 :排序后的数组以原地形式体现(交换数组元素,无需返回值;LeetCode 上对
nums做原地修改后返回nums)
示例 1:
nums = [3,5,2,1,6,4] -> 输出 [3,5,1,6,2,4]([1,6,2,5,3,4] 也是有效答案)
示例 2:
nums = [6,6,5,6,3,8] -> 例如 [6,6,5,6,3,8] 本身也合法(6<=6>=5<=6>=3<=8)
约束:
- 1 <=
nums.length<= 5 * 10^4;0 <=nums[i]<= 10^4;输入保证至少存在一个有效答案。
解题思路
贪心相邻交换 / 排序后重排
- 解法一(贪心动相邻交换):从左到右处理下标 i,若 i 为偶数(0、2、4...)要求
numsi <= numsi+1,否则交换;若 i 为奇数要求 numsi >= numsi+1,否则交换。
处理完所有相邻对即满足摆动序。正确性来自"只要所有相邻对满足方向,全局就满足摆动序",
且每步交换不会破坏已经处理好的左侧约束。 - 解法二(排序后重排):先排序,然后把排序后序列的前半段与后半段交替插回:能够保证
a0<=a1>=a2...。适合先讲思路再引到解法一,工程上排序 O(n log n)。 - 解法三(排序 + 两两交换相邻):排序后对 (1,2),(3,4),... 每组交换,也可以得到合法摆动序;
同样 O(n log n)。仅讨论,不展开代码(与解法二重复度高)。
为什么相邻贪心就够:对任意相邻对 (i, i+1) 的方向已按要求;交换相邻元素只影响局部关系,
因为交换后 numsi 与 numsi-1 的关系在步骤 i-1 已经满足且交换方向与该约束相容
(奇数位变大/偶数位变小均不破坏前一个约束),归纳可得全局合法。
代码实现
java
public class Solution {
public void wiggleSort(int[] nums) {
for (int i = 0; i + 1 < nums.length; i++) {
if (((i & 1) == 0 && nums[i] > nums[i + 1])
|| ((i & 1) == 1 && nums[i] < nums[i + 1])) {
swap(nums, i, i + 1);
}
}
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
public void wiggleSortSort(int[] nums) {
java.util.Arrays.sort(nums);
for (int i = 1; i + 1 < nums.length; i += 2) {
swap(nums, i, i + 1);
}
}
private static boolean isValid(int[] nums) {
for (int i = 0; i + 1 < nums.length; i++) {
if ((i & 1) == 0 && nums[i] > nums[i + 1]) {
return false;
}
if ((i & 1) == 1 && nums[i] < nums[i + 1]) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] t1 = {3, 5, 2, 1, 6, 4};
s.wiggleSort(t1);
check(isValid(t1), "示例1 贪心 -> 合法摆动序 " + java.util.Arrays.toString(t1));
int[] t2 = {6, 6, 5, 6, 3, 8};
s.wiggleSort(t2);
check(isValid(t2), "示例2 贪心 -> 合法摆动序 " + java.util.Arrays.toString(t2));
int[] t3 = {1};
s.wiggleSort(t3);
check(isValid(t3), "单元素 -> 合法");
int[] t4 = {1, 2};
s.wiggleSort(t4);
check(isValid(t4), "两元素 -> " + java.util.Arrays.toString(t4));
int[] t5 = {2, 1};
s.wiggleSort(t5);
check(isValid(t5), "降序两元素 -> " + java.util.Arrays.toString(t5));
int[] t6 = {5, 5, 5, 5};
s.wiggleSort(t6);
check(isValid(t6), "全相同 -> " + java.util.Arrays.toString(t6));
// 排序解法验证
int[] u1 = {3, 5, 2, 1, 6, 4};
s.wiggleSortSort(u1);
check(isValid(u1), "排序解法示例1 -> " + java.util.Arrays.toString(u1));
int[] u2 = {6, 6, 5, 6, 3, 8};
s.wiggleSortSort(u2);
check(isValid(u2), "排序解法示例2 -> " + java.util.Arrays.toString(u2));
int[] u3 = {4, 3, 2, 1};
s.wiggleSortSort(u3);
check(isValid(u3), "排序解法降序 -> " + java.util.Arrays.toString(u3));
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n)(一趟线性扫描),空间 O(1);
解法二:时间 O(n log n)(排序),空间 O(1)(原地)或 O(n)(辅助数组)。
281. Zigzag Iterator(锯齿迭代器)
题目描述
给出两个整数向量 v1 和 v2,请你实现一个迭代器,交替返回它们的元素。
实现 ZigzagIterator 类:
- ZigzagIterator(
List<Integer> v1,List<Integer> v2) 用两个向量 v1 和 v2 初始化对象; booleanhasNext()如果迭代器还有元素返回true,否则返回false;intnext()返回迭代器的当前元素并将迭代器移动到下一个元素。- 输入 :
List<Integer> v1,List<Integer> v2 - 输出 :交替访问的顺序(重复调用
next直到hasNext返回false)
示例 1:
v1 = [1,2], v2 = [3,4,5,6] -> [1,3,2,4,5,6]
示例 2:
v1 = [1], v2 = [] -> [1]
示例 3:
v1 = [], v2 = [1] -> [1]
进阶:假如给你 k 个向量呢?例如 v1=1,2,3, v2=4,5,6,7, v3=8,9 -> 1,4,8,2,5,9,3,6,7。
解题思路
双下标循环轮转 / k 向量统一队列
- 解法一(双下标 + 循环轮转):维护 cur 表示当前要出的向量下标,indexesi 记录第 i 个
向量的已用下标。next 取出 vectorscur 的当前元素后把指针后移,cur = (cur + 1) % n
轮转;hasNext 循环跳过已耗尽的向量(indexescur == vectorscur.size()),若转回
起点说明全部为空返回 false。均摊 O(1)。 - 解法二(统一队列,天然支持 k 个向量):把"仍有元素的向量下标"放进队列,每次 next
从队首取一个下标出元素,若该向量还有剩余元素则重新入队尾。实现最简洁,直接解答
进阶问题:任意 k 个向量都能工作。时间均摊 O(1),空间 O(k)。
为什么解法一不复制数据:两个数组仅仅记录指针,不复制任何元素,适合流式大数据;
若扩展为 k 个向量,只需把下标数组 size 改为向量个数(参见本类构造的三次元写法)。
代码实现
java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class ZigzagIterator {
private int cur; // 当前轮转到哪个向量
private int[] indexes; // 每个向量的已用下标
private List<List<Integer>> vectors;
public ZigzagIterator(List<Integer> v1, List<Integer> v2) {
vectors = new ArrayList<>();
vectors.add(v1);
vectors.add(v2);
indexes = new int[vectors.size()];
cur = 0;
}
public int next() {
List<Integer> v = vectors.get(cur);
int res = v.get(indexes[cur]);
indexes[cur]++;
cur = (cur + 1) % vectors.size();
return res;
}
public boolean hasNext() {
int start = cur;
while (indexes[cur] == vectors.get(cur).size()) { // 当前向量已耗尽则换下一个
cur = (cur + 1) % vectors.size();
if (cur == start) { // 转回起点,全部为空
return false;
}
}
return true;
}
public static class ZigzagQueue {
private Queue<Integer> q; // 仍有元素的向量下标
private List<List<Integer>> vectors;
private int[] pos;
private ZigzagQueue(List<Integer>... vs) {
vectors = new ArrayList<>();
pos = new int[vs.length];
q = new LinkedList<>();
for (int i = 0; i < vs.length; i++) {
vectors.add(vs[i]);
if (!vs[i].isEmpty()) {
q.offer(i);
}
}
}
public int next() {
int i = q.poll();
int res = vectors.get(i).get(pos[i]);
pos[i]++;
if (pos[i] < vectors.get(i).size()) { // 该向量还有剩余则回到队尾
q.offer(i);
}
return res;
}
public boolean hasNext() {
return !q.isEmpty();
}
}
public static void main(String[] args) {
// 示例 1:v1 = [1,2], v2 = [3,4,5,6] -> [1,3,2,4,5,6]
ZigzagIterator it = new ZigzagIterator(
new ArrayList<>(java.util.Arrays.asList(1, 2)),
new ArrayList<>(java.util.Arrays.asList(3, 4, 5, 6)));
List<Integer> out = new ArrayList<>();
while (it.hasNext()) {
out.add(it.next());
}
check(out.equals(java.util.Arrays.asList(1, 3, 2, 4, 5, 6)), "示例1 -> [1,3,2,4,5,6]");
// 示例 2:v1 = [1], v2 = [] -> [1]
ZigzagIterator it2 = new ZigzagIterator(
new ArrayList<>(java.util.Arrays.asList(1)),
new ArrayList<>());
out = new ArrayList<>();
while (it2.hasNext()) {
out.add(it2.next());
}
check(out.equals(java.util.Arrays.asList(1)), "示例2 -> [1]");
// 示例 3:v1 = [], v2 = [1] -> [1]
ZigzagIterator it3 = new ZigzagIterator(
new ArrayList<>(),
new ArrayList<>(java.util.Arrays.asList(1)));
out = new ArrayList<>();
while (it3.hasNext()) {
out.add(it3.next());
}
check(out.equals(java.util.Arrays.asList(1)), "示例3 -> [1]");
// 两个向量都为空
ZigzagIterator it4 = new ZigzagIterator(new ArrayList<>(), new ArrayList<>());
check(!it4.hasNext(), "空向量 hasNext -> false");
// 解法二:队列版 + k=3 进阶示例 v1=[1,2,3], v2=[4,5,6,7], v3=[8,9]
ZigzagQueue q = new ZigzagQueue(
new ArrayList<>(java.util.Arrays.asList(1, 2, 3)),
new ArrayList<>(java.util.Arrays.asList(4, 5, 6, 7)),
new ArrayList<>(java.util.Arrays.asList(8, 9)));
out = new ArrayList<>();
while (q.hasNext()) {
out.add(q.next());
}
check(out.equals(java.util.Arrays.asList(1, 4, 8, 2, 5, 9, 3, 6, 7)),
"队列版 k=3 -> [1,4,8,2,5,9,3,6,7]");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:next/hasNext 均摊 O(1),空间 O(k)(k 为向量个数,本题 k=2);
解法二:next/hasNext 均摊 O(1),空间 O(k)。
282. Expression Add Operators(给表达式添加运算符)
题目描述
给定一个仅包含数字 0-9 的字符串 num 和一个目标值整数 target,在 num 的数字之间
添加二元运算符(不是一元)+、- 或 *,返回所有能够得到 target 的表达式。
注意:
- 返回表达式中的操作数不应该包含前导零(数字 "0" 本身可以);一个数字可以包含多个数位。
- 返回列表的顺序不限。
- 输入 :
Stringnum,inttarget(1 <=num.length<= 10,num仅含数字,-2^31 <=target<= 2^31 - 1) - 输出 :
List<String>(所有能得到target的表达式)
示例 1:
num = "123", target = 6 -> ["1+2+3", "1*2*3"]
示例 2:
num = "232", target = 8 -> ["2*3+2", "2+3*2"]
示例 3:
num = "3456237490", target = 9191 -> []
约束:
- 1 <=
num.length<= 10;num仅含数字 '0'-'9';-2^31 <=target<= 2^31 - 1。
解题思路
DFS 回溯(枚举切分 + 乘法回退)/ 字符串拼接简化版
- 解法一(DFS + 回溯,StringBuilder 拼接):从 pos 开始枚举下一个操作数 numpos...i
(含前导零判断:若 numpos == '0' 且 i > pos 则不允许拼接,直接 break)。维护
prev(上一个操作数)与 curr(当前表达式的值)两个 long:
· 首个操作数:直接放数字,prev = curr = next;
· '+':curr + next,prev = next;
· '-':curr - next,prev = -next;
· '*':乘法优先于加减,需要回退:curr - prev + prev * next,prev = prev * next。
到 num 末尾时若 curr == target 则记录表达式。用 long 避免中间积溢出(乘法可能远超 int)。 - 解法二(字符串拼接版):思路完全相同,只是每次递归用 path + op + next 生成新字符串,
无需手动回退,代价是每个分支多付出 O(长度) 的字符串复制,适合作为对照理解。
为什么需要 prev:乘法会把"上一个加/减项"整体替换成它的 next 倍,因此单靠 curr 无法计算,
必须记录最近一次加入的操作数(含符号),乘时做 curr - prev + prev * next 的差额修正。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
private List<String> ans = new ArrayList<>();
private char[] cs;
public List<String> addOperators(String num, int target) {
ans = new ArrayList<>();
cs = num.toCharArray();
dfs(0, 0L, 0L, new StringBuilder(), target);
return ans;
}
private void dfs(int pos, long prev, long curr, StringBuilder path, long target) {
if (pos == cs.length) {
if (curr == target) {
ans.add(path.toString());
}
return;
}
for (int i = pos; i < cs.length; i++) {
if (i != pos && cs[pos] == '0') { // 前导零:多位数字不能以 0 开头
break;
}
long next = 0;
for (int j = pos; j <= i; j++) {
next = next * 10 + (cs[j] - '0');
}
int len = path.length();
if (pos == 0) { // 首个操作数:运算符不允许出现在表达式开头
path.append(next);
dfs(i + 1, next, next, path, target);
path.setLength(len);
} else {
path.append('+').append(next); // '+'
dfs(i + 1, next, curr + next, path, target);
path.setLength(len);
path.append('-').append(next); // '-'
dfs(i + 1, -next, curr - next, path, target);
path.setLength(len);
path.append('*').append(next); // '*': 回退上一次加减再乘
dfs(i + 1, prev * next, curr - prev + prev * next, path, target);
path.setLength(len);
}
}
}
public List<String> addOperatorsSimple(String num, int target) {
List<String> res = new ArrayList<>();
dfsSimple(num, 0, 0L, 0L, "", target, res);
return res;
}
private void dfsSimple(String num, int pos, long prev, long curr, String path,
long target, List<String> res) {
if (pos == num.length()) {
if (curr == target) {
res.add(path);
}
return;
}
for (int i = pos; i < num.length(); i++) {
if (i != pos && num.charAt(pos) == '0') {
break;
}
long next = Long.parseLong(num.substring(pos, i + 1));
if (pos == 0) {
dfsSimple(num, i + 1, next, next, path + next, target, res);
} else {
dfsSimple(num, i + 1, next, curr + next, path + "+" + next, target, res);
dfsSimple(num, i + 1, -next, curr - next, path + "-" + next, target, res);
dfsSimple(num, i + 1, prev * next, curr - prev + prev * next,
path + "*" + next, target, res);
}
}
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:num = "123", target = 6 -> ["1+2+3", "1*2*3"]
checkSame(s.addOperators("123", 6), new String[]{"1+2+3", "1*2*3"}, "示例1 -> [1+2+3, 1*2*3]");
// 示例 2:num = "232", target = 8 -> ["2*3+2", "2+3*2"]
checkSame(s.addOperators("232", 8), new String[]{"2*3+2", "2+3*2"}, "示例2 -> [2*3+2, 2+3*2]");
// 示例 3:num = "3456237490", target = 9191 -> []
check(s.addOperators("3456237490", 9191).isEmpty(), "示例3 -> []");
// 前导零:num = "105", target = 5 -> ["1*0+5", "10-5"]
checkSame(s.addOperators("105", 5), new String[]{"1*0+5", "10-5"}, "前导零 -> [1*0+5, 10-5]");
// 单字符:num = "0", target = 0 -> ["0"]
checkSame(s.addOperators("0", 0), new String[]{"0"}, "单字符 -> [0]");
// 解法二一致性抽查
checkSame(s.addOperatorsSimple("232", 8), new String[]{"2*3+2", "2+3*2"}, "解法二 示例2");
System.out.println("全部测试通过");
}
private static void checkSame(List<String> got, String[] expect, String name) {
List<String> exp = new ArrayList<>(java.util.Arrays.asList(expect));
exp.sort(null);
List<String> g = new ArrayList<>(got);
g.sort(null);
check(g.equals(exp), name);
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间复杂度 O(4^(n-1))(每个间隙有 +、-、*、拼接 四种选择),
空间 O(n)(递归深度 + 表达式长度);
解法二:时间同 O(4^(n-1)) 但常数更大(字符串复制),空间 O(n)。
283. Move Zeroes(移动零)
题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的
相对顺序。请注意,必须在不复制数组的情况下原地对数组进行操作。
- 输入 :
int[]nums(1 <=nums.length<= 104,-231 <=nums[i]<= 2^31 - 1) - 输出 :原地修改
nums,不返回任何东西
示例 1:
nums = [0,1,0,3,12] -> [1,3,12,0,0]
示例 2:
nums = [0] -> [0]
进阶:你能尽量减少完成的操作次数吗?
约束:
- 1 <=
nums.length<= 104;-231 <=nums[i]<= 2^31 - 1。
解题思路
快慢指针覆盖 / 快慢指针交换
- 解法一(覆盖 + 补零):slow 指向"下一个非零元素应放置的位置"。遍历 fast,
遇到非零就把 numsfast 写入 numsslow 并 slow++;遍历结束后从 slow 到末尾全部
填 0。每个元素至多读写各一次,操作次数较少且逻辑直观。 - 解法二(原地交换):同样用 slow 指向下一个非零位置,但遇到非零时交换
numsslow 与 numsfast 再 slow++(当 slow == fast 时与自己交换,无害)。
优势:全程只需一次遍历,且"非零元素"在数组左侧的比例较高时写回次数更少;
零元素自然"漂移"到右侧,无需补零步骤。
为什么两种写法都保持相对顺序:slow 只指向已确定的位置,且只与当前元素交换/覆盖,
从未跳过任何非零元素,因此非零元素在数组中的先后次序保持不变。
代码实现
java
import java.util.Arrays;
public class Solution {
public void moveZeroes(int[] nums) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != 0) {
nums[slow++] = nums[fast];
}
}
for (int i = slow; i < nums.length; i++) {
nums[i] = 0;
}
}
public void moveZeroesSwap(int[] nums) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != 0) {
int t = nums[slow];
nums[slow] = nums[fast];
nums[fast] = t;
slow++;
}
}
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:[0,1,0,3,12] -> [1,3,12,0,0]
int[] a1 = {0, 1, 0, 3, 12};
s.moveZeroes(a1);
check(Arrays.equals(a1, new int[]{1, 3, 12, 0, 0}), "示例1 -> [1,3,12,0,0]");
// 示例 2:[0] -> [0]
int[] a2 = {0};
s.moveZeroes(a2);
check(Arrays.equals(a2, new int[]{0}), "示例2 -> [0]");
// 全零数组
int[] a3 = {0, 0, 0};
s.moveZeroes(a3);
check(Arrays.equals(a3, new int[]{0, 0, 0}), "全零 -> [0,0,0]");
// 无零数组
int[] a4 = {1, 2, 3};
s.moveZeroes(a4);
check(Arrays.equals(a4, new int[]{1, 2, 3}), "无零 -> [1,2,3]");
// 解法二:交换版抽查
int[] b1 = {0, 1, 0, 3, 12};
s.moveZeroesSwap(b1);
check(Arrays.equals(b1, new int[]{1, 3, 12, 0, 0}), "解法二 示例1");
int[] b2 = {1, 0, 0, 2};
s.moveZeroesSwap(b2);
check(Arrays.equals(b2, new int[]{1, 2, 0, 0}), "解法二 [1,0,0,2]");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(1);
解法二:时间 O(n),空间 O(1)。
284. Peeking Iterator(窥视迭代器)
题目描述
请你在设计一个迭代器,在集成现有迭代器拥有的 hasNext 和 next 操作的基础上,
还额外支持 peek 操作。
实现 PeekingIterator 类:
PeekingIterator(Iterator<Integer>nums) 使用指定整数迭代器nums初始化迭代器;intnext()返回数组中的下一个元素,并将指针移动到下个元素处;booleanhasNext()如果数组中存在下一个元素,返回true;否则返回false;intpeek()返回数组中的下一个元素,但不移动指针。
注意:每种语言可能有不同的构造函数和迭代器Iterator,但均支持intnext()和
booleanhasNext()两个函数。- 输入 :操作序列(构造 +
peek/next/hasNext调用) - 输出 :按操作返回相应结果
示例:"`PeekingIterator`","`next`","`peek`","`next`","`next`","`hasNext`"\[\[1,2,3\]\],\[\],\[\],\[\],\[\],\[\]\] -\> \[`null`,1,2,2,3,`false`
约束: - 1 <=
nums.length<= 1000;1 <=nums[i]<= 1000;对next和peek的调用均有效;
next、hasNext和peek最多调用 1000 次。
解题思路
预取缓存 / 惰性缓存
- 解法一(构造时预取):构造时若底层迭代器有元素,立即把首元素取出来存到缓存 next 中,
并维护布尔 hasBuffered 表示缓存是否有效。peek 直接返回缓存;next 返回缓存后立刻从
底层迭代器再取一个补位(没有则清空缓存);hasNext 即 hasBuffered。所有操作 O(1)。 - 解法二(惰性预取):构造时不取,peek 时若缓存为空(!loaded)才从底层迭代器取一次;
next 返回缓存并标记未加载;hasNext 为 loaded || 底层迭代器 hasNext。效果与解法一
等价,区别仅在预取的时机,适合"peek 可能完全不调用"的场景。
为什么需要缓存:peek 要求"看一眼但不前进",但底层 Iterator 只有 next(取出并前进),
因此必须把取出的元素暂存,等下次 next 时再消费,这是几乎所有语言的标准解法。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
public class PeekingIterator implements Iterator<Integer> {
private Iterator<Integer> it; // 底层迭代器
private Integer cache; // 缓存的下一个元素
private boolean hasBuffered; // 缓存是否有效
public PeekingIterator(Iterator<Integer> iterator) {
it = iterator;
hasBuffered = it.hasNext();
cache = hasBuffered ? it.next() : null;
}
public Integer peek() {
if (!hasBuffered) {
throw new NoSuchElementException();
}
return cache;
}
@Override
public Integer next() {
if (!hasBuffered) {
throw new NoSuchElementException();
}
Integer res = cache;
hasBuffered = it.hasNext();
cache = hasBuffered ? it.next() : null;
return res;
}
@Override
public boolean hasNext() {
return hasBuffered;
}
public static class PeekingIteratorLazy implements Iterator<Integer> {
private Iterator<Integer> it;
private Integer cache; // 缓存值
private boolean loaded; // 缓存是否已加载
public PeekingIteratorLazy(Iterator<Integer> iterator) {
it = iterator;
}
public Integer peek() {
if (!loaded) {
if (!it.hasNext()) {
throw new NoSuchElementException();
}
cache = it.next();
loaded = true;
}
return cache;
}
@Override
public Integer next() {
if (!loaded) {
if (!it.hasNext()) {
throw new NoSuchElementException();
}
cache = it.next();
}
Integer res = cache;
loaded = false;
return res;
}
@Override
public boolean hasNext() {
return loaded || it.hasNext();
}
}
public static void main(String[] args) {
// 示例:[1,2,3],操作序列 -> [null,1,2,2,3,false]
PeekingIterator p = new PeekingIterator(Arrays.asList(1, 2, 3).iterator());
check(p.next() == 1, "next -> 1");
check(p.peek() == 2, "peek -> 2");
check(p.next() == 2, "next -> 2");
check(p.next() == 3, "next -> 3");
check(!p.hasNext(), "hasNext -> false");
// 单元素迭代器
PeekingIterator p2 = new PeekingIterator(Arrays.asList(7).iterator());
check(p2.peek() == 7, "单元素 peek -> 7");
check(p2.hasNext(), "单元素 hasNext -> true");
check(p2.next() == 7, "单元素 next -> 7");
check(!p2.hasNext(), "单元素 hasNext -> false");
// 连续 peek 不移动指针
PeekingIterator p3 = new PeekingIterator(Arrays.asList(1, 2).iterator());
check(p3.peek() == 1 && p3.peek() == 1, "连续 peek 不移动指针");
check(p3.next() == 1 && p3.peek() == 2, "peek 后 next -> 1, 再 peek -> 2");
// 解法二:惰性预取版抽查
PeekingIteratorLazy lazy = new PeekingIteratorLazy(Arrays.asList(1, 2, 3).iterator());
check(lazy.next() == 1, "lazy next -> 1");
check(lazy.peek() == 2, "lazy peek -> 2");
check(lazy.next() == 2 && lazy.next() == 3, "lazy next -> 2,3");
check(!lazy.hasNext(), "lazy hasNext -> false");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:peek/next/hasNext 均 O(1),空间 O(1)(仅缓存一个元素);
解法二:peek/next/hasNext 均 O(1),空间 O(1)。
285. Inorder Successor in BST(二叉搜索树中的中序后继)
题目描述
给定一棵二叉搜索树和其中的一个节点 p,找到该节点在树中的中序后继。如果节点
没有中序后继,请返回 null。节点 p 的后继是值比 p.val 大的节点中键值最小的节点。
- 输入 :
TreeNoderoot,TreeNodep(树中节点数目 1, 10^4,-10^5 <=Node.val<= 10^5,
树中各节点的值均保证唯一,p为树中真实节点) - 输出 :
TreeNode(p的中序后继,若不存在返回null)
示例 1:
root = [2,1,3], p = 1 -> 2(1 的中序后继是 2)
示例 2:
root = [5,3,6,2,4,null,null,1], p = 6 -> null
约束:
- 节点数目在 1, 10\^4 内;-10^5 <=
Node.val<= 10^5;各节点值唯一;p是树中节点。
解题思路
BST 性质递归/迭代 / 中序遍历
- 解法一(利用 BST 性质迭代):从 root 出发维护候选后继 succ。若当前节点值
root.val > p.val,则当前节点是"可能的后继"(比 p 大且是最接近的),向左子树找更小
的候选;若 root.val <= p.val,则需要更大的值,向右子树找。最终 succ 即为答案。
严格 O(log n)-O(h)(h 为树高)。不需要任何额外空间,也不依赖中序遍历。 - 解法二(迭代中序遍历):用栈做中序遍历,遇到节点时若上一节点是 p(指针相等),
则当前节点就是后继;使用节点引用相等(node == p)判断而非值相等(值唯一,两者皆可,
引用判断更严谨)。时间 O(n)、空间 O(h)。
为什么值比较成立:BST 中根大于全部左子树、小于全部右子树,且值唯一,
"比 p.val 大的最小节点"恰好沿搜索路径最后一次"向左走"的顶点。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
TreeNode succ = null;
while (root != null) {
if (root.val > p.val) { // 当前节点比 p 大,可能是后继,向左找更小的候选
succ = root;
root = root.left;
} else { // 当前节点不比 p 大,需向右找更大的值
root = root.right;
}
}
return succ;
}
public TreeNode inorderSuccessorInorder(TreeNode root, TreeNode p) {
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode node = root;
boolean found = false;
while (node != null || !stack.isEmpty()) {
while (node != null) {
stack.push(node);
node = node.left;
}
node = stack.pop();
if (found) { // p 已出栈,当前即中序后继
return node;
}
if (node == p) { // 以引用相等定位 p
found = true;
}
node = node.right;
}
return null;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:root = [2,1,3], p = 1 -> 2
TreeNode n1a = new TreeNode(1);
TreeNode root1 = new TreeNode(2, n1a, new TreeNode(3));
check(s.inorderSuccessor(root1, n1a) == root1, "示例1 -> 2");
// p 是最小值 1 的另一种验证(解法二)
check(s.inorderSuccessorInorder(root1, n1a) == root1, "解法二 示例1 -> 2");
// 示例 2:root = [5,3,6,2,4,null,null,1], p = 6 -> null
TreeNode n6 = new TreeNode(6);
TreeNode root2 = new TreeNode(5,
new TreeNode(3, new TreeNode(2, new TreeNode(1), null), new TreeNode(4)),
n6);
check(s.inorderSuccessor(root2, n6) == null, "示例2 -> null");
check(s.inorderSuccessorInorder(root2, n6) == null, "解法二 示例2 -> null");
// p = 4([5,3,6,2,4,...] 中 4 的后继是 5)
TreeNode n4 = root2.left.right;
check(s.inorderSuccessor(root2, n4) == root2, "p=4 -> 5");
// p = 2(2 的后继是 3)
TreeNode n2 = root2.left.left;
check(s.inorderSuccessor(root2, n2) == root2.left, "p=2 -> 3");
// p = 1(1 的后继是 2)
TreeNode n1b = root2.left.left.left;
check(s.inorderSuccessor(root2, n1b) == root2.left.left, "p=1 -> 2");
// 单节点树:root=[1], p=1 -> null
TreeNode only = new TreeNode(1);
check(s.inorderSuccessor(only, only) == null, "单节点 -> null");
// 右子树的左后代:root=[4,2,6,1,3,5,7], p=4 -> 5
TreeNode n5 = new TreeNode(5);
TreeNode root3 = new TreeNode(4,
new TreeNode(2, new TreeNode(1), new TreeNode(3)),
new TreeNode(6, n5, new TreeNode(7)));
check(s.inorderSuccessor(root3, root3) == n5, "p=4(根) -> 5");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(h)(h 为树高,最坏 O(n)),空间 O(1);
解法二:时间 O(n),空间 O(h)。
286. Walls and Gates(墙与门)
题目描述
你被给定一个 m x n 的二维网格 rooms,网格中有以下三种可能的初始化值:
- -1 表示墙或是障碍物;
- 0 表示一扇门;
- INF 无限表示一个空的房间,使用 2^31 - 1 = 2147483647 代表 INF。
你要给每个空房间位上填上该房间到最近门的距离,如果无法到达门,则填 INF 即可。 - 输入 :
int[][]rooms(m== rooms.length,1 <=m<= 500?,rooms 元素为 -1 / 0 / INF) - 输出:原地修改 rooms,不返回任何东西
示例 1:
rooms = [[2147483647,-1,0,2147483647],
2147483647,2147483647,2147483647,-1,
2147483647,-1,2147483647,-1,
0,-1,2147483647,2147483647\]
-> \[3,-1,0,1,2,2,1,-1,1,-1,2,-1,0,-1,3,4]
示例 2:
rooms = [[-1]] -> [[-1]]
示例 3:
rooms = [[2147483647]] -> [[2147483647]]
示例 4:
rooms = [[0]] -> [[0]]
约束:
m,n<= 250(经典限定);每格为 -1、0 或 2147483647。
解题思路
多源 BFS / 暴力 DFS 每个房间
- 解法一(多源 BFS,推荐):所有门(0)同时作为 BFS 起点入队。BFS 分层扩散,第一次
访问到某个空房间时,其距离必然是最短路径距离(BFS 按层序保证)。从门出发逐层
扩展,把 INF 改成 dist;房间未被访问则保持 INF。时间 O(mn),每个格子至多入队一次。 - 解法二(对每个房间 DFS):对每个 INF 房间做一次 DFS/BFS 找最近的门,逐个更新。
实现直观但最坏情况每个格子的搜索都可能扩散至全图,复杂度退化 O((mn)^2),
仅作为暴力对照。
为什么门必须一次全部入队:多源 BFS 与"把超源连向所有门"等价,从超源的第 0 层就是
所有门,之后每层距离 +1,天然保证每个房间取到的是最近门距离,而不是逐个门反复搜索。
代码实现
java
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
public class Solution {
private static final int INF = Integer.MAX_VALUE;
private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public void wallsAndGates(int[][] rooms) {
if (rooms == null || rooms.length == 0) {
return;
}
int m = rooms.length, n = rooms[0].length;
Deque<int[]> q = new ArrayDeque<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (rooms[i][j] == 0) { // 所有门同时入队
q.offer(new int[]{i, j});
}
}
}
while (!q.isEmpty()) {
int[] cur = q.poll();
for (int[] d : DIRS) {
int nx = cur[0] + d[0], ny = cur[1] + d[1];
if (nx < 0 || nx >= m || ny < 0 || ny >= n) {
continue;
}
if (rooms[nx][ny] == INF) { // 首次到达即最短距离
rooms[nx][ny] = rooms[cur[0]][cur[1]] + 1;
q.offer(new int[]{nx, ny});
}
}
}
}
public void wallsAndGatesBrute(int[][] rooms) {
if (rooms == null || rooms.length == 0) {
return;
}
int m = rooms.length, n = rooms[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (rooms[i][j] == INF) {
rooms[i][j] = bfsDist(rooms, i, j);
}
}
}
}
private int bfsDist(int[][] rooms, int sr, int sc) {
int m = rooms.length, n = rooms[0].length;
boolean[][] seen = new boolean[m][n];
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{sr, sc, 0});
seen[sr][sc] = true;
while (!q.isEmpty()) {
int[] cur = q.poll();
for (int[] d : DIRS) {
int nx = cur[0] + d[0], ny = cur[1] + d[1];
if (nx < 0 || nx >= m || ny < 0 || ny >= n || seen[nx][ny] || rooms[nx][ny] == -1) {
continue;
}
if (rooms[nx][ny] == 0) {
return cur[2] + 1;
}
seen[nx][ny] = true;
q.offer(new int[]{nx, ny, cur[2] + 1});
}
}
return INF;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
int[][] r1 = {
{INF, -1, 0, INF},
{INF, INF, INF, -1},
{INF, -1, INF, -1},
{0, -1, INF, INF}};
s.wallsAndGates(r1);
check(Arrays.deepEquals(r1, new int[][]{
{3, -1, 0, 1},
{2, 2, 1, -1},
{1, -1, 2, -1},
{0, -1, 3, 4}}), "示例1");
// 示例 2:[[-1]]
int[][] r2 = {{-1}};
s.wallsAndGates(r2);
check(Arrays.deepEquals(r2, new int[][]{{-1}}), "示例2");
// 示例 3:[[INF]]
int[][] r3 = {{INF}};
s.wallsAndGates(r3);
check(Arrays.deepEquals(r3, new int[][]{{INF}}), "示例3");
// 示例 4:[[0]]
int[][] r4 = {{0}};
s.wallsAndGates(r4);
check(Arrays.deepEquals(r4, new int[][]{{0}}), "示例4");
// 解法二:暴力对照示例 1
int[][] r5 = {
{INF, -1, 0, INF},
{INF, INF, INF, -1},
{INF, -1, INF, -1},
{0, -1, INF, INF}};
s.wallsAndGatesBrute(r5);
check(Arrays.deepEquals(r5, new int[][]{
{3, -1, 0, 1},
{2, 2, 1, -1},
{1, -1, 2, -1},
{0, -1, 3, 4}}), "暴力版 示例1");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(mn),空间 O(mn)(队列最坏容纳所有门/格子);
解法二:时间 O((mn)^2),空间 O(mn)(递归栈/队列)。
287. Find the Duplicate Number(寻找重复数)
题目描述
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1, `n` 范围内(包括 1 和 n),
可知至少存在一个重复的整数。假设 nums 只有一个重复的整数,返回这个重复的数。
设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 的额外空间。
- 输入 :
int[]nums(1 <=n<= 10^5,nums.length==n+ 1,1 <=nums[i]<=n,
nums中只有一个整数出现两次或多次,其余整数均只出现一次) - 输出 :
int(重复的数)
示例 1:
nums = [1,3,4,2,2] -> 2
示例 2:
nums = [3,1,3,4,2] -> 3
示例 3:
nums = [3,3,3,3,3] -> 3
约束:
- 1 <=
n<= 10^5;nums.length==n+ 1;1 <=nums[i]<=n;
nums中只有一个整数出现两次或多次,其余均一次。
解题思路
Floyd 判圈(快慢指针)/ 二进制位统计 / 二分值域
- 解法一(Floyd 判圈,推荐):把数组看作链表:i -> numsi。由于存在重复元素,必然
存在环(重复元素即环入口)。慢指针每次走一步、快指针每次走两步,相遇后把快指针
重置为起点,两者再各走一步,再次相遇的位置就是环入口,即重复数。
零修改、O(1) 空间,完美满足题目限制。 - 解法二(二进制位统计):统计 1,n 期望出现次数与 nums 实际出现次数,某一位上
"实际次数 > 期望次数"则重复数在该位为 1,逐位拼出答案。时间 O(n log n),符合
O(1) 空间(只用固定位数变量),但常数略大。 - 解法三(值域二分):按值域中位数 mid 统计 nums 中 <= mid 的个数 cnt;若 cnt > mid
说明重复数在左半区,否则在右半区。时间 O(n log n),空间 O(1)。
为什么弗洛伊德判圈成立:下标可以当链表节点,numsi 是 next 指针;由于值在 1,n 且
长度 n+1,必然存在一个值被至少两个不同下标指向,构成入度为 2 的环。环入口 = 重复值。
代码实现
java
import java.util.Arrays;
public class Solution {
public int findDuplicate(int[] nums) {
int slow = nums[0];
int fast = nums[nums[0]];
while (slow != fast) { // 快慢指针必在环中相遇
slow = nums[slow];
fast = nums[nums[fast]];
}
fast = 0; // 快指针回到起点
while (slow != fast) { // 再各走一步,相遇处即环入口
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
public int findDuplicateBit(int[] nums) {
int n = nums.length - 1;
int ans = 0;
for (int b = 0; (1 << b) <= n; b++) { // 覆盖 n 的二进制位数
int cnt = 0, exp = 0;
for (int x : nums) {
if ((x & (1 << b)) != 0) {
cnt++; // 实际出现次数中该位为 1
}
}
for (int x = 1; x <= n; x++) {
if ((x & (1 << b)) != 0) {
exp++; // [1,n] 期望出现次数中该位为 1
}
}
if (cnt > exp) {
ans |= (1 << b);
}
}
return ans;
}
public int findDuplicateBinary(int[] nums) {
int lo = 1, hi = nums.length - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
int cnt = 0;
for (int x : nums) {
if (x <= mid) {
cnt++;
}
}
if (cnt > mid) { // 重复数在 [lo, mid]
hi = mid;
} else { // 在 (mid, hi]
lo = mid + 1;
}
}
return lo;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
check(s.findDuplicate(new int[]{1, 3, 4, 2, 2}) == 2, "示例1 -> 2");
// 示例 2
check(s.findDuplicate(new int[]{3, 1, 3, 4, 2}) == 3, "示例2 -> 3");
// 示例 3
check(s.findDuplicate(new int[]{3, 3, 3, 3, 3}) == 3, "示例3 -> 3");
// n=1 边界:[1,1]
check(s.findDuplicate(new int[]{1, 1}) == 1, "n=1 -> 1");
// 解法二:位统计抽查
check(s.findDuplicateBit(new int[]{1, 3, 4, 2, 2}) == 2, "位统计 示例1");
check(s.findDuplicateBit(new int[]{3, 1, 3, 4, 2}) == 3, "位统计 示例2");
check(s.findDuplicateBit(new int[]{2, 2, 2, 2, 2}) == 2, "位统计 全2");
// 解法三:二分抽查
check(s.findDuplicateBinary(new int[]{1, 3, 4, 2, 2}) == 2, "二分 示例1");
check(s.findDuplicateBinary(new int[]{3, 1, 3, 4, 2}) == 3, "二分 示例2");
check(s.findDuplicateBinary(new int[]{1, 1}) == 1, "二分 n=1 -> 1");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(1);
解法二:时间 O(n log n),空间 O(1);
解法三:时间 O(n log n),空间 O(1)。
288. Unique Word Abbreviation(单词的唯一缩写)
题目描述
单词的缩写需要遵循 <起始字母><中间字母数><结尾字母> 这样的格式。如果单词只有
两个字符,那么它就是它自身的缩写。
实现 ValidWordAbbr 类:
- ValidWordAbbr(
String[]dictionary) 使用单词字典 dictionary 初始化对象; booleanisUnique(Stringword) 如果满足下述任意一个条件,返回true;否则返回false:
· 字典 dictionary 中没有任何其他单词的缩写与该单词word的缩写相同;
· 字典 dictionary 中所有缩写与该单词word的缩写相同的单词都与word相同。- 输入 :
String[]dictionary + 若干次isUnique查询 - 输出 :
boolean
示例:dictionary = ["deer","door","cake","card"]
isUnique("dear") ->false("dear" 与 "d2r","deer"/"door"/"card" 都缩成 d2r,且与 dear 不同)
isUnique("cart") ->true("cart" 与 "c2t",字典中只有画 "cake" 缩成 c2k,无冲突)
isUnique("cane") ->false("cane" c2e 与 "cake" c2k 不同缩?------见下文详细说明)
isUnique("make") ->true("make" m2e 无冲突)
约束: - 0 <= dictionary.length <= 5000;单词仅含小写字母。
解题思路
哈希表:缩写 -> 单词集合 / 缩写 -> (唯一单词, 冲突标记)
- 解法一(缩写 -> 集合):构造时对 dictionary 每个词生成缩写 abbr,存入
Map<String, List>。isUnique(word) 取该缩写对应列表:列表为空 -> true;
列表非空且列表中的所有单词都与 word 相同(去重后只有一个且等于 word)-> true;
否则 false。理解最直接,但集合占用较大。 - 解法二(缩写 -> 计数与唯一单词,推荐):Map<String, int2+> 只记录
{count, 唯一单词或 sentinel}。更紧凑:缩写出现 1 次且该单词 == word 时唯一;
出现 >1 次则除非全部相同(即 count 个相同单词)也允许 ------ 但"全部相同"等价于
"仅一个不同单词拼写",用<唯一单词, 出现次数>对即可判定。
本实现用 Map<String, Map<String,Integer>> 记录每个缩写的(单词 -> 次数),
既支持"无冲突"也支持"冲突但全相同",逻辑最清晰。
为什么需要区分"字典里有没有该缩写":isUnique 的定义不是"字典里没有缩写冲突",而是
"冲突的词都和我一样才叫唯一"。因此必须能区分 0 冲突、1 个冲突词且相同、1 个冲突词
且不同、多个冲突词四种情形,简单 bool 计数不够用。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ValidWordAbbr {
private Map<String, Map<String, Integer>> map; // 缩写 -> (单词 -> 出现次数)
public ValidWordAbbr(String[] dictionary) {
map = new HashMap<>();
for (String w : dictionary) {
String key = abbr(w);
map.computeIfAbsent(key, k -> new HashMap<>())
.merge(w, 1, Integer::sum);
}
}
private String abbr(String w) {
if (w.length() <= 2) {
return w;
}
return "" + w.charAt(0) + (w.length() - 2) + w.charAt(w.length() - 1);
}
public boolean isUnique(String word) {
Map<String, Integer> groups = map.get(abbr(word));
if (groups == null || groups.isEmpty()) {
return true; // 字典中没有任何缩写冲突
}
// 所有缩写相同的词都必须与 word 相同
for (Map.Entry<String, Integer> e : groups.entrySet()) {
if (!e.getKey().equals(word)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
// 示例:dictionary = ["deer","door","cake","card"]
ValidWordAbbr v = new ValidWordAbbr(new String[]{"deer", "door", "cake", "card"});
check(!v.isUnique("dear"), "dear -> false");
check(!v.isUnique("door"), "door -> false(字典含 door 且同缩写的 deer 与其不同)");
check(v.isUnique("cart"), "cart -> true");
// "cane" 缩写 c2e,"cake" 缩写也是 c2e(c + "ak" 两个字符 + e)且 cake != cane -> false
check(!v.isUnique("cane"), "cane -> false(c2e 与 cake 冲突)");
// 空字典:所有词都唯一
ValidWordAbbr empty = new ValidWordAbbr(new String[]{});
check(empty.isUnique("a"), "空字典 a -> true");
check(empty.isUnique("hello"), "空字典 hello -> true");
// 字典中有完全相同单词(重复词只算一次冲突,但"所有相同"应仍唯一)
ValidWordAbbr dup = new ValidWordAbbr(new String[]{"hello", "hello", "world"});
check(dup.isUnique("hello"), "重复 hello -> true(冲突词都等于 hello)");
check(dup.isUnique("helo"), "helo(h2o? 与 hello h3o 不同)-> true");
check(!dup.isUnique("hallo"), "hallo(h3o) 与 hello(h3o) 同缩写不同词 -> false");
// 长度 2 单词
ValidWordAbbr two = new ValidWordAbbr(new String[]{"do", "go"});
check(two.isUnique("do"), "do -> true(do 缩写即 do,字典无其他 do)");
// "fo" 缩写即 "fo",字典中无 "fo" -> true
check(two.isUnique("fo"), "fo -> true(无 fo 缩写冲突)");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
构造:时间 O(L)(L 为字典总字符数),空间 O(L);
isUnique:时间 O(1)(Map 读写),空间 O(1)。
289. Game of Life(生命游戏)
题目描述
给定一个包含 m x n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都
具有一个初始状态:1 即为活细胞(live),或 0 即为死细胞(dead)。每个细胞与其八个
相邻位置(水平,垂直,对角线)的细胞遵循四条生存定律:
- 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
- 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
- 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
- 如果死细胞周围正好有三个活细胞,则该位置死细胞复活。
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的(出生和死亡同时发生)。
给定board的当前状态,原地更新board到下一个状态。不需要返回任何东西。
- 输入 :
int[][]board(m==board.length,n==board[0].length,1 <=m,n<= 25?) - 输出 :原地更新,
void
示例 1:
board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]] -> [[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
约束:
m,n较大(常为 25/若干百);每个细胞同时更新。
解题思路
借用状态位原地编码 / 复制快照
- 解法一(状态位编码,推荐):同时更新要求"旧状态"在每个细胞计算时仍可用。用
两位二进制同时存新旧状态:低位置 1 表示"下个状态活",高位置 1 表示"当前状态活"
(即 LIVE=2 表示旧活新死,DEAD=3 表示旧死新活......本实现用 +10 增量标记法)。
实际实现:对每个活细胞周围 8 格 neighborsij++(只增不减标记被活邻居影响);
若原为活:邻居 <2 或 >3 -> 0,否则保留 1;原为死:邻居恰 3 -> 1。
使用带符号拆分避免破坏原值------直接复制快照更稳妥直观。 - 解法二(复制快照):先深拷贝一份 board 作为 old,按 old 计算每个格子的邻居活数,
再更新 board。最简单不易错,代价是 O(mn) 额外空间。
为什么必须"同时"更新:若逐个格子就地更新,后更新的格子会用到已经被改动的邻居,
结果取决于遍历顺序,与"同步演化"定义不符;因此要么保留快照,要么用编码法。
代码实现
java
import java.util.Arrays;
public class Solution {
private static final int[][] DIRS = {
{-1, -1}, {-1, 0}, {-1, 1},
{0, -1}, {0, 1},
{1, -1}, {1, 0}, {1, 1}};
public void gameOfLife(int[][] board) {
int m = board.length, n = board[0].length;
// 增量法:活细胞对周围 8 格 +2 标记"被活邻居影响"
// 约定:board[i][j] 为 -1 表示"原死但被 3 个活邻居复活";>=2 表示"原活"(计数含 2 基数)
// 简化实现:先遍历统计每个格子的活邻居数并编码,再统一解码。
int[][] copy = copy(board); // 为避免与原值混淆,此处用快照(解法一因此退化为 O(mn) 空间,
// 但主解法展示"原地编码"思路------见 gameOfLifeInPlace,它完全 O(1))
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int live = liveNeighbors(copy, i, j);
if (copy[i][j] == 1) {
board[i][j] = (live < 2 || live > 3) ? 0 : 1;
} else {
board[i][j] = (live == 3) ? 1 : 0;
}
}
}
}
public void gameOfLifeInPlace(int[][] board) {
int m = board.length, n = board[0].length;
// 步骤 1:统计并写入"新状态位"(bit1),bit0 保留旧状态
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int live = liveNeighborsInPlace(board, i, j);
boolean willLive;
if ((board[i][j] & 1) == 1) { // 旧活
willLive = live == 2 || live == 3;
} else { // 旧死
willLive = live == 3;
}
if (willLive) {
board[i][j] |= 2; // 写入新状态位
}
}
}
// 步骤 2:右移一位,新状态落到位 0
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
board[i][j] >>= 1;
}
}
}
private int liveNeighbors(int[][] b, int i, int j) {
int cnt = 0;
for (int[] d : DIRS) {
int x = i + d[0], y = j + d[1];
if (x >= 0 && x < b.length && y >= 0 && y < b[0].length && b[x][y] == 1) {
cnt++;
}
}
return cnt;
}
private int liveNeighborsInPlace(int[][] b, int i, int j) {
int cnt = 0;
for (int[] d : DIRS) {
int x = i + d[0], y = j + d[1];
if (x >= 0 && x < b.length && y >= 0 && y < b[0].length && (b[x][y] & 1) == 1) {
cnt++;
}
}
return cnt;
}
private int[][] copy(int[][] board) {
int m = board.length, n = board[0].length;
int[][] c = new int[m][];
for (int i = 0; i < m; i++) {
c[i] = Arrays.copyOf(board[i], n);
}
return c;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
int[][] b1 = {{0, 1, 0}, {0, 0, 1}, {1, 1, 1}, {0, 0, 0}};
s.gameOfLife(b1);
check(Arrays.deepEquals(b1, new int[][]{{0, 0, 0}, {1, 0, 1}, {0, 1, 1}, {0, 1, 0}}), "示例1");
// 示例 2:board = [[1,1],[1,0]] -> [[1,1],[1,1]]
int[][] b2 = {{1, 1}, {1, 0}};
s.gameOfLife(b2);
check(Arrays.deepEquals(b2, new int[][]{{1, 1}, {1, 1}}), "示例2");
// 原地编码版:示例 1
int[][] b3 = {{0, 1, 0}, {0, 0, 1}, {1, 1, 1}, {0, 0, 0}};
s.gameOfLifeInPlace(b3);
check(Arrays.deepEquals(b3, new int[][]{{0, 0, 0}, {1, 0, 1}, {0, 1, 1}, {0, 1, 0}}), "原地编码 示例1");
// 原地编码版:示例 2
int[][] b4 = {{1, 1}, {1, 0}};
s.gameOfLifeInPlace(b4);
check(Arrays.deepEquals(b4, new int[][]{{1, 1}, {1, 1}}), "原地编码 示例2");
// 单格
int[][] b5 = {{1}};
s.gameOfLifeInPlace(b5);
check(Arrays.deepEquals(b5, new int[][]{{0}}), "单格 1 -> 0");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(mn),空间 O(1)(若用快照则 O(mn));
解法二:时间 O(mn),空间 O(mn)。
290. Word Pattern(单词规律)
题目描述
给定一种规律 pattern 和一个字符串 s,判断 s 是否遵循相同的规律。遵循指完全
匹配:pattern 里的每个字母和字符串 s 中的每个非空单词之间存在着双向连接的对应规律。
具体来说:
- pattern 中的每个字母都恰好映射到
s中的一个唯一单词; s中的每个唯一单词都恰好映射到 pattern 中的一个字母;- 没有两个字母映射到同一个单词,也没有两个单词映射到同一个字母。
- 输入 :
Stringpattern,Strings(1 <= pattern.length <= 300,pattern 仅含小写英文字母,
1 <=s.length<= 3000,s仅含小写英文字母和空格 ' ',s不含前后空格,s中单词间由单个空格分隔) - 输出 :
boolean
示例 1:
pattern = "abba", s = "dog cat cat dog" -> true
示例 2:
pattern = "abba", s = "dog cat cat fish" -> false
示例 3:
pattern = "aaaa", s = "dog cat cat dog" -> false
约束:
- 1 <= pattern.length <= 300;1 <=
s.length<= 3000;pattern 仅含小写字母;
s仅含小写字母与单个空格,无前后空格。
解题思路
双 Map 双向映射 / 单 Map 配反向校验
- 解法一(双 Map):charToWord 与 wordToChar 两个哈希表维护双向映射。遍历 pattern
每个字符与按空格切分后的单词表,若任一方向出现"已映射到不同对象"则返回 false,
否则双向写入。最后还需要长度相等(单词个数 == pattern 长度),否则不匹配。 - 解法二(单 Map + 索引值技巧):Map<Object,Integer> 用一个 Map 同时为字符与单词
分配"首次出现的索引",patterni 与 wordi 必须是同一个索引,否则冲突。
用 Map<Object,Integer> 即可让 char 与 String 共用一张表(装箱后 key 不同类互不干扰)。
为什么双 Map 必须成对出现:只记录 pattern 字符 -> 单词会漏掉类似 pattern="ab",
s="x x" 的反向冲突(两个字母映射同一个单词);成对记录双向映射才能在两个方向拦截。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
public boolean wordPattern(String pattern, String s) {
String[] words = s.split(" ");
if (words.length != pattern.length()) {
return false;
}
Map<Character, String> c2w = new HashMap<>();
Map<String, Character> w2c = new HashMap<>();
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
String w = words[i];
if (c2w.containsKey(c) && !c2w.get(c).equals(w)) {
return false;
}
if (w2c.containsKey(w) && w2c.get(w) != c) {
return false;
}
c2w.put(c, w);
w2c.put(w, c);
}
return true;
}
public boolean wordPatternOneMap(String pattern, String s) {
String[] words = s.split(" ");
if (words.length != pattern.length()) {
return false;
}
Map<Object, Integer> last = new HashMap<>();
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
String w = words[i];
// 首次出现索引一致才允许建立映射
if (!last.containsKey(c) && !last.containsKey(w)) {
last.put(c, i);
last.put(w, i);
} else if (last.get(c) != last.get(w)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
check(s.wordPattern("abba", "dog cat cat dog"), "示例1 -> true");
// 示例 2
check(!s.wordPattern("abba", "dog cat cat fish"), "示例2 -> false");
// 示例 3
check(!s.wordPattern("aaaa", "dog cat cat dog"), "示例3 -> false");
// 单词数不等
check(!s.wordPattern("abba", "dog cat cat"), "长度不等 -> false");
// 反向冲突:pattern="ab", s="x x"
check(!s.wordPattern("ab", "x x"), "反向冲突 ab/x x -> false");
// 单映射
check(s.wordPattern("a", "dog"), "单映射 a/dog -> true");
// pattern="aaa", s="aa aa aa"
check(s.wordPattern("aaa", "aa aa aa"), "aaa/aa aa aa -> true");
// 解法二抽查
check(s.wordPatternOneMap("abba", "dog cat cat dog"), "单Map 示例1");
check(!s.wordPatternOneMap("abba", "dog cat cat fish"), "单Map 示例2");
check(!s.wordPatternOneMap("ab", "x x"), "单Map 反向冲突");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n)(n 为 pattern 长度 / 单词个数),空间 O(n);
解法二:时间 O(n),空间 O(n)。
291. Word Pattern II(单词规律 II)
题目描述
给你一种规律 pattern 和一个字符串 s,请你判断 s 是否和 pattern 的规律相匹配。
如果存在单个字符到非空字符串的双射映射,那么字符串 s 匹配 pattern,即:如果 pattern
中的每个字符都被它映射到的字符串替换,那么最终的字符串则为 s。双射意味着映射双方
一一对应,不会存在两个字符映射到同一个字符串,也不会存在一个字符分别映射到两个不同
的字符串。
- 输入 :
Stringpattern,Strings(1 <= pattern.length,s.length<= 20,pattern 和s仅含小写
英文字母) - 输出 :
boolean
示例 1:
pattern = "abab", s = "redblueredblue" -> true('a'->"red",'b'->"blue")
示例 2:
pattern = "aaaa", s = "asdasdasdasd" -> true('a'->"asd")
示例 3:
pattern = "aabb", s = "xyzabcxzyabc" -> false
约束:
- 1 <= pattern.length,
s.length<= 20;两者仅含小写英文字母。
解题思路
回溯 + 双向 HashMap / 回溯 + 数组映射(固定小写字母)
- 解法一(回溯 + 双 Map):DFS 同时推进 pattern 下标 i 与 s 下标 j。对当前字符 c,
枚举 s 从 j 开始的每个切分长度 k(取子串 next):若 c 已映射且映射 != next 剪枝;
若 word 已被其他字符映射(wordToPat 冲突)剪枝;若 c 未映射则先建立双射再递归;
递归完成后回退。若 i == len(pattern) 且 j == len(s) 则成功。 - 解法二(回溯 + 数组 + 首尾哨兵):因为 pattern 仅含 26 个小写字母,可用
String26 记录 pattern 字符 -> 单词,用 Map<String,Character> 记录反向。
实质与解法一相同,仅数据结构不同,便于展示两种容器写法。
为什么必须双射判定:与 290 题不同,本题的单词切分不确定,若只记录方向一,会出现
两个不同 pattern 字符映射到同一个单词片段(如 a->"x"、b->"x")而误判成功,
因此反向映射(单词已被谁占用)必须同步检查。
为什么 DFS 枚举切分是唯一思路:因为单词长度未知,只有把每种子串切分尝试完,
才能判定是否存在一组映射;这是典型的"排列组合式"回溯,剪枝依赖双向映射冲突。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
private Map<Character, String> pat2word;
private Map<String, Character> word2pat;
public boolean wordPatternMatch(String pattern, String s) {
pat2word = new HashMap<>();
word2pat = new HashMap<>();
return dfs(pattern, 0, s, 0);
}
private boolean dfs(String pattern, int pi, String s, int sj) {
if (pi == pattern.length()) {
return sj == s.length(); // pattern 用完,s 也恰好吃完才算匹配
}
char c = pattern.charAt(pi);
if (pat2word.containsKey(c)) { // 该字符已有固定映射
String word = pat2word.get(c);
if (!s.startsWith(word, sj)) {
return false; // 当前位置前缀不符
}
return dfs(pattern, pi + 1, s, sj + word.length());
}
// 该字符尚未映射:枚举 s 从 j 开始的每一种切分(非空)
for (int k = sj + 1; k <= s.length(); k++) {
String word = s.substring(sj, k);
if (word2pat.containsKey(word)) {
continue; // 该单词已被其他字符占用,不能复用
}
pat2word.put(c, word);
word2pat.put(word, c);
if (dfs(pattern, pi + 1, s, k)) {
return true;
}
pat2word.remove(c); // 回溯
word2pat.remove(word);
}
return false;
}
public boolean wordPatternMatchArr(String pattern, String s) {
String[] p2w = new String[26];
Map<String, Character> w2p = new HashMap<>();
return dfsArr(pattern, 0, s, 0, p2w, w2p);
}
private boolean dfsArr(String pattern, int pi, String s, int sj,
String[] p2w, Map<String, Character> w2p) {
if (pi == pattern.length()) {
return sj == s.length();
}
int idx = pattern.charAt(pi) - 'a';
if (p2w[idx] != null) {
String word = p2w[idx];
if (!s.startsWith(word, sj)) {
return false;
}
return dfsArr(pattern, pi + 1, s, sj + word.length(), p2w, w2p);
}
for (int k = sj + 1; k <= s.length(); k++) {
String word = s.substring(sj, k);
if (w2p.containsKey(word)) {
continue;
}
p2w[idx] = word;
w2p.put(word, pattern.charAt(pi));
if (dfsArr(pattern, pi + 1, s, k, p2w, w2p)) {
return true;
}
p2w[idx] = null;
w2p.remove(word);
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
check(s.wordPatternMatch("abab", "redblueredblue"), "示例1 -> true");
// 示例 2
check(s.wordPatternMatch("aaaa", "asdasdasdasd"), "示例2 -> true");
// 示例 3:aabb, xyzabcxzyabc -> false
check(!s.wordPatternMatch("aabb", "xyzabcxzyabc"), "示例3 -> false");
// 长度不足:pattern 3 个字符但 s 仅 2 个字符,切不出 3 个非空片段 -> false
check(!s.wordPatternMatch("abc", "ab"), "长度不足 -> false");
// 单字符
check(s.wordPatternMatch("a", "xyz"), "单字符 xyz -> true");
// 双射强校验:pattern="ab", s="xx":a->x, b->x 冲突 false
check(!s.wordPatternMatch("ab", "xx"), "ab/xx 反向冲突 -> false");
// 两个相同字符无法匹配两个不同片段:a->"a" 后剩余 "b" 与 "a" 冲突
check(!s.wordPatternMatch("aa", "ab"), "aa/ab -> false");
// 解法二抽查
check(s.wordPatternMatchArr("abab", "redblueredblue"), "数组版 示例1");
check(!s.wordPatternMatchArr("ab", "xx"), "数组版 反向冲突");
check(s.wordPatternMatchArr("a", "xyz"), "数组版 单字符");
check(!s.wordPatternMatchArr("abc", "ab"), "数组版 长度不足");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(2^n)(最坏切分数指数级,n 为 s 长度;实际剪枝很快),
空间 O(n)(递归深度 + 映射表);
解法二:时间同上,空间 O(26 + 单词数)。
292. Nim Game(Nim 游戏)
题目描述
你和你的朋友,两个人一起玩 Nim 游戏:
- 桌子上有一堆石头;你们轮流进行自己的回合,你作为先手;
- 每一回合,轮到的人拿掉 1 - 3 块石头;
- 拿掉最后一块石头的人就是获胜者。
假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为n的情况
下赢得游戏。如果可以赢,返回true;否则返回false。 - 输入 :
intn(1 <=n<= 2^31 - 1) - 输出 :
boolean
示例 1:
n = 4 -> false(无论先手拿 1/2/3 块,朋友都能拿走剩下全部石头)
示例 2:
n = 1 -> true(先手直接拿走最后一块)
示例 3:
n = 2 -> true
约束:
- 1 <=
n<= 2^31 - 1。
解题思路
博弈论:4 的倍数必败 / 记忆化搜索(小数据验证)
- 解法一(数学结论,推荐):n % 4 != 0 先手必胜。因为每轮两人合计可以始终拿走
4 块(先手拿 x 块后手补 4-x 块),所以若 n 是 4 的倍数,后手可以用"凑 4"策略保证
自己拿到最后一块;若 n 不是 4 的倍数,先手先拿 n % 4 块,把局面变成 4 的倍数交给后手,
自己便复制"凑 4"策略获胜。时间 O(1)、空间 O(1)。 - 解法二(记忆化搜索):设 f(i) 表示剩余 i 块时当前玩家是否必胜:存在一步取 1/2/3 后
使对方必败即必胜,即 f(i) = !f(i-1) || !f(i-2) || !f(i-3)。用数组递推或记忆化,
可以得出 f 在模 4 上的模式(false,true,true,true 循环)。适合小 n 验证结论。
为什么"凑 4"成立:每回合每人取 1-3 块,两人合计一定可以做到 4 块(先手取 x,
后手取 4-x,1 <= 4-x <= 3),从而把"回合配对"转化为控制权问题。
代码实现
java
public class Solution {
public boolean canWinNim(int n) {
return n % 4 != 0;
}
public boolean canWinNimDp(int n) {
if (n % 4 == 0) { // 结论直接返回,便于对照
return false;
}
// 小规模用 DP 验证:滚动三个状态
boolean f1 = true, f2 = true, f3 = true; // i = 1,2,3 均必胜
for (int i = 4; i <= n; i++) {
boolean cur = !f1 || !f2 || !f3; // 取 i-1/i-2/i-3 后对手必败即可
f1 = f2;
f2 = f3;
f3 = cur;
}
return n <= 3 ? true : f3;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
check(!s.canWinNim(4), "n=4 -> false");
// 示例 2
check(s.canWinNim(1), "n=1 -> true");
// 示例 3
check(s.canWinNim(2), "n=2 -> true");
// 边界与循环
check(s.canWinNim(3), "n=3 -> true");
check(!s.canWinNim(8), "n=8 -> false");
check(s.canWinNim(5), "n=5 -> true");
// 大 n:2147483647 % 4 = 3 -> true(无溢出问题,取模即可)
check(s.canWinNim(2147483647), "n=INT_MAX -> true");
// DP 版抽查(小规模)
for (int n = 1; n <= 20; n++) {
if (s.canWinNim(n) != s.canWinNimDp(n)) {
check(false, "DP 与数学结论不一致 n=" + n);
}
}
check(true, "DP 1..20 与数学结论一致");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(1),空间 O(1);
解法二:时间 O(n),空间 O(n) 或 O(1)(滚动数组)。
293. Flip Game(翻转游戏)
题目描述
你和朋友玩一个叫做「翻转游戏」的游戏。游戏规则如下:给你一个字符串
currentState,其中只含 '+' 和 '-'。你和朋友轮流将连续的两个 "++" 反转成 "--"。
当一方无法进行有效的翻转时便意味着游戏结束,则另一方获胜。
计算并返回一次有效操作后,字符串 currentState 所有的可能状态,返回结果可以按任意顺序
排列。如果不存在可能的有效操作,请返回一个空列表 \[\]。
- 输入 :
StringcurrentState(1 <=currentState.length<= 500,currentState仅含 '+' 和 '-') - 输出 :
List<String>(所有可能的一次操作后状态)
示例 1:
currentState = "++++" -> ["--++","+--+","++--"]
示例 2:
currentState = "+" -> []
约束:
- 1 <=
currentState.length<= 500;仅含 '+' 与 '-'。
解题思路
线性扫描 / 递归枚举(两写法本质一致)
- 解法一(线性扫描替换,推荐):从左到右扫描,每遇到相邻两个 "++" 就把它们替换成
"--" 加入结果。用 StringBuilder 构造新串避免反复拼接字符串。全程 O(n)。 - 解法二(substring 拼接版):扫描到 "++" 时直接 currentState0...i + "--" +
currentStatei+2...,与解法一等价,仅写法不同,逻辑最直白。
为什么只有一个可移动的"操作位":题目要求只做一次有效操作后返回所有可能状态,
不是求博弈胜负(那是 294 题),因此无需搜索所有后续局面;线性位置扫描即可穷尽全部
可能的单步操作。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
public List<String> generatePossibleNextMoves(String currentState) {
List<String> res = new ArrayList<>();
StringBuilder sb = new StringBuilder(currentState);
for (int i = 0; i + 1 < currentState.length(); i++) {
if (currentState.charAt(i) == '+' && currentState.charAt(i + 1) == '+') {
sb.setCharAt(i, '-');
sb.setCharAt(i + 1, '-');
res.add(sb.toString());
sb.setCharAt(i, '+'); // 还原
sb.setCharAt(i + 1, '+');
}
}
return res;
}
public List<String> generatePossibleNextMovesSub(String currentState) {
List<String> res = new ArrayList<>();
for (int i = 0; i + 1 < currentState.length(); i++) {
if (currentState.charAt(i) == '+' && currentState.charAt(i + 1) == '+') {
res.add(currentState.substring(0, i) + "--" + currentState.substring(i + 2));
}
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:++++ -> ["--++","+--+","++--"]
checkSame(s.generatePossibleNextMoves("++++"),
new String[]{"--++", "+--+", "++--"}, "示例1 -> [--++,+--+,++--]");
// 示例 2:+ -> []
check(s.generatePossibleNextMoves("+").isEmpty(), "示例2 -> []");
// 无 ++
check(s.generatePossibleNextMoves("--").isEmpty(), "-- -> []");
check(s.generatePossibleNextMoves("-+-").isEmpty(), "-+- -> []");
// 单组:++
checkSame(s.generatePossibleNextMoves("++"), new String[]{"--"}, "++ -> [--]");
// 解法二抽查
checkSame(s.generatePossibleNextMovesSub("++++"),
new String[]{"--++", "+--+", "++--"}, "解法二 示例1");
check(s.generatePossibleNextMovesSub("-+-").isEmpty(), "解法二 -+- -> []");
System.out.println("全部测试通过");
}
private static void checkSame(List<String> got, String[] expect, String name) {
List<String> exp = new ArrayList<>(Arrays.asList(expect));
exp.sort(null);
List<String> g = new ArrayList<>(got);
g.sort(null);
check(g.equals(exp), name);
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n)(每次构造 O(n),最多 O(n) 个位置,整体 O(n^2) 最坏,
平均 O(n)),空间 O(n)(结果集);
解法二:时间 O(n^2)(substring 每步复制),空间 O(n)。
294. Flip Game II(翻转游戏 II)
题目描述
你和朋友玩一个叫做「翻转游戏」的游戏。游戏规则如下:给你一个字符串
currentState,其中只含 '+' 和 '-'。你和朋友轮流将连续的两个 "++" 反转成 "--"。
当一方无法进行有效的翻转时便意味着游戏结束,则另一方获胜。默认每个人都会采取最优
策略。请你写出一个函数来判定起始玩家是否存在必胜的方案:如果存在,返回 true;
否则,返回 false。
- 输入 :
StringcurrentState(1 <=currentState.length<= 30?经典约束,仅含 '+' 和 '-') - 输出 :
boolean
示例 1:
currentState = "++++" -> true(起始玩家可将中间的 "++" 翻转变为 "+--+" 从而得胜)
示例 2:
currentState = "+" -> false
约束:
- 1 <=
currentState.length<= 30;仅含 '+' 与 '-'。
解题思路
DFS + 记忆化(Sprague-Grundy 的直观版)/ 直接递归(无记忆)
- 解法一(DFS + 备忘录):对状态 s,若存在一次翻转使"对方必败",则当前玩家必胜。
递归式:canWin(s) = 存在 i 使 flip(s,i) 满足 !canWin(flip(s,i))。
用 HashMap<String,Boolean> 记忆已算状态,避免指数级重复计算。
下一步状态字符串去重可进一步压缩,但直接用串联 "" + 代替也行。 - 解法二(纯递归,无记忆):不缓存任何状态,代码最短,适合说明递归本质;
最坏情况随状态数增长明显变慢(指数级),仅用于小规模演示。
为什么必须"存在一步使对方必败":双方最优,轮到"对方"时对方也走最优步,
当前玩家只有找到一条"让对手无论怎么走都输"的分支才叫必胜;等价地,递归定义
canWin(s) = 存在合法移动 -> !canWin(next)。"+" 与 "-"不对称,翻转后字符结构决定
后续,因此必须枚举全部翻转点。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
private Map<String, Boolean> memo = new HashMap<>();
public boolean canWin(String currentState) {
if (memo.containsKey(currentState)) {
return memo.get(currentState);
}
StringBuilder sb = new StringBuilder(currentState);
for (int i = 0; i + 1 < currentState.length(); i++) {
if (currentState.charAt(i) == '+' && currentState.charAt(i + 1) == '+') {
sb.setCharAt(i, '-');
sb.setCharAt(i + 1, '-');
String next = sb.toString();
sb.setCharAt(i, '+'); // 还原
sb.setCharAt(i + 1, '+');
if (!canWin(next)) { // 存在一步使对方必败
memo.put(currentState, true);
return true;
}
}
}
memo.put(currentState, false);
return false;
}
public boolean canWinNoMemo(String currentState) {
for (int i = 0; i + 1 < currentState.length(); i++) {
if (currentState.charAt(i) == '+' && currentState.charAt(i + 1) == '+') {
String next = currentState.substring(0, i) + "--" + currentState.substring(i + 2);
if (!canWinNoMemo(next)) {
return true;
}
}
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
check(s.canWin("++++"), "示例1 -> true");
// 示例 2
check(!s.canWin("+"), "示例2 -> false");
// 无操作位
check(!s.canWin("--"), "-- -> false");
check(!s.canWin("+-+-"), "+-+- -> false");
// 单组:++ -> true(翻成 -- 后对方无法操作必败,当前玩家必胜)
check(s.canWin("++"), "++ -> true");
// canWin("+--+"):++ 只在两端(+--+ 中 i=0 是 '+-',i=1 是 '--',i=2 是 '-+',无 ++)-> false
check(!s.canWin("+--+"), "+--+ -> false");
// long:++++++(6 个 +)
check(s.canWin("++++++"), "++++++ -> true");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n! ) 最坏、实际受记忆化约束远小于此,O(状态数 x 状态变化数),
空间 O(状态数)(备忘录);
解法二:时间指数级,空间 O(n) 递归深度。
295. Find Median from Data Stream(数据流的中位数)
题目描述
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是
两个中间值的平均值。
实现 MedianFinder 类:
MedianFinder()初始化MedianFinder对象;voidaddNum(intnum) 将数据流中的整数num添加到数据结构中;doublefindMedian()返回到目前为止所有元素的中位数。与实际答案相差 10^-5 以内的
答案将被接受。- 输入 :操作序列(构造 +
addNum/findMedian) - 输出 :
findMedian返回double
示例:"`MedianFinder`","`addNum`","`addNum`","`findMedian`","`addNum`","`findMedian`"\[\],\[1\],\[2\],\[\],\[3\],\[\]\] -\> \[`null`,`null`,`null`,1.5,`null`,2.0
约束: - 最多调用
addNum和findMedian共 5 * 10^4 次;-10^5 <=num<= 10^5。
解题思路
两个堆(大顶 + 小顶)/ 排序数组二分插入
- 解法一(双堆,推荐):维护 left 大顶堆存较小一半、right 小顶堆存较大一半,并保持
left.size == right.size 或 left.size == right.size + 1。addNum 先入 left,再平衡
(若 left 峰值 > right 峰谷则交换,或简单地把较大者弹出到另一边);findMedian 时
若两堆同大小取两堆顶平均,否则取 left 堆顶(奇数时左多 1 个)。
每种操作 O(log n),是数据流中位数的标准解法。 - 解法二(有序插入):维护一个 List,addNum 时用二分查找插入位置再 add 到指定下标
(ArrayList 插入 O(n) 移动),findMedian 直接按下标取值。实现直观,但 addNum
退化 O(n),不适合大数据流。
为什么双堆不破坏有序性:堆只保证堆顶有序,两个堆各自堆顶分别是"左半最大"与"右半最小",
只要保证左半所有元素 <= 右半所有元素(通过峰值交换维护),中位数就只跟这两个堆顶有关。
代码实现
java
import java.util.Collections;
import java.util.PriorityQueue;
public class MedianFinder {
private PriorityQueue<Integer> left; // 大顶堆:较小一半
private PriorityQueue<Integer> right; // 小顶堆:较大一半
public MedianFinder() {
left = new PriorityQueue<>(Collections.reverseOrder());
right = new PriorityQueue<>();
}
public void addNum(int num) {
if (left.isEmpty() || num <= left.peek()) {
left.offer(num);
} else {
right.offer(num);
}
// 平衡:|left| 最多比 |right| 大 1
if (left.size() > right.size() + 1) {
right.offer(left.poll());
} else if (right.size() > left.size()) {
left.offer(right.poll());
}
}
public double findMedian() {
if (left.size() > right.size()) { // 奇数个,left 多 1
return left.peek();
}
return (left.peek() + right.peek()) / 2.0; // 偶数个,取平均
}
public static class MedianFinderList {
private java.util.List<Integer> list;
public MedianFinderList() {
list = new java.util.ArrayList<>();
}
public void addNum(int num) {
int lo = 0, hi = list.size();
while (lo < hi) { // 二分找插入位置
int mid = (lo + hi) >>> 1;
if (list.get(mid) < num) {
lo = mid + 1;
} else {
hi = mid;
}
}
list.add(lo, num);
}
public double findMedian() {
int n = list.size();
if ((n & 1) == 1) {
return list.get(n / 2);
}
return (list.get(n / 2 - 1) + list.get(n / 2)) / 2.0;
}
}
public static void main(String[] args) {
// 示例:add 1,2 -> 1.5;add 3 -> 2.0
MedianFinder mf = new MedianFinder();
mf.addNum(1);
mf.addNum(2);
check(Math.abs(mf.findMedian() - 1.5) < 1e-9, "add 1,2 -> 1.5");
mf.addNum(3);
check(Math.abs(mf.findMedian() - 2.0) < 1e-9, "add 3 -> 2.0");
// 单元素
MedianFinder mf2 = new MedianFinder();
mf2.addNum(5);
check(Math.abs(mf2.findMedian() - 5.0) < 1e-9, "单元素 -> 5.0");
// 乱序流
MedianFinder mf3 = new MedianFinder();
for (int x : new int[]{7, 1, 9, 3, 5}) {
mf3.addNum(x); // 有序 1,3,5,7,9
}
check(Math.abs(mf3.findMedian() - 5.0) < 1e-9, "乱序 5 个 -> 5.0");
mf3.addNum(6); // 1,3,5,6,7,9
check(Math.abs(mf3.findMedian() - 5.5) < 1e-9, "6 个 -> 5.5");
// 解法二:有序列表版抽查
MedianFinderList ml = new MedianFinderList();
ml.addNum(1); ml.addNum(2);
check(Math.abs(ml.findMedian() - 1.5) < 1e-9, "列表版 add 1,2 -> 1.5");
ml.addNum(3);
check(Math.abs(ml.findMedian() - 2.0) < 1e-9, "列表版 add 3 -> 2.0");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:addNum O(log n),findMedian O(1),空间 O(n);
解法二:addNum O(n)(二分 O(log n) + 移位 O(n)),findMedian O(1),空间 O(n)。
296. Best Meeting Point(最佳的碰头地点)
题目描述
给你一个 m x n 的二进制网格 grid,其中 1 表示某个朋友的家所处的位置。返回
最小的总行走距离。总行走距离是朋友们家到碰头地点的距离之和。使用曼哈顿距离计算:
distance(p1, p2) = |p2.x - p1.x| + |p2.y - p1.y|
- 输入 :
int[][]grid(m==grid.length,n==grid[0].length,1 <=m,n<= 200,
grid[i][j]为 0 或 1,朋友数目至少 1 个) - 输出 :
int(最小的总行走距离)
示例 1:
grid = [[1,0,0,0,1],[0,0,0,0,0],[0,0,1,0,0]] -> 6
三人住在 (0,0)、(0,4)、(2,2),碰头点在 (0,0)/(0,2)/(2,0)/(2,2) 处总距离 6。
约束:
m,n<= 200;朋友数至少 1。
解题思路
中位数定理(坐标分离,推荐)/ 枚举所有格子
- 解法一(分别取行列中位数,推荐):曼哈顿距离可分离为 X 方向距离与 Y 方向距离之和,
因此问题等价于"在一维数轴上选一点使到若干固定点的距离和最小",该点取所有点的
中位数即可(中位数两侧的个数一致,任何偏移都会使总距离不降)。
实现:把所有行坐标与列坐标分别收集排序,取中位数(交点即最佳地点),
再累加两者到中位数的距离。时间 O(mn log(mn)) 或 O(mn)(计数桶)。 - 解法二(枚举每个格子):对每个格子计算到所有朋友家的曼哈顿距离和,取最小。
复杂度 O(mn * 朋友数),作为暴力对照。
为什么曼哈顿可分可各自取中位:|x-x0|+|y-y0| 对 x0、y0 独立求和,min 可以直接分解为
min_x 与 min_y 之和;一维情形的最优点必然在中位数(含两个中位数之间的整段)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Solution {
public int minTotalDistance(int[][] grid) {
int m = grid.length, n = grid[0].length;
List<Integer> rows = new ArrayList<>();
List<Integer> cols = new ArrayList<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
rows.add(i);
cols.add(j);
}
}
}
Collections.sort(rows);
Collections.sort(cols);
int mr = rows.get(rows.size() / 2); // 行中位数
int mc = cols.get(cols.size() / 2); // 列中位数
int res = 0;
for (int r : rows) {
res += Math.abs(r - mr);
}
for (int c : cols) {
res += Math.abs(c - mc);
}
return res;
}
public int minTotalDistanceBrute(int[][] grid) {
List<int[]> friends = new ArrayList<>();
int m = grid.length, n = grid[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
friends.add(new int[]{i, j});
}
}
}
int best = Integer.MAX_VALUE;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int d = 0;
for (int[] f : friends) {
d += Math.abs(f[0] - i) + Math.abs(f[1] - j);
}
best = Math.min(best, d);
}
}
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
int[][] g1 = {{1, 0, 0, 0, 1}, {0, 0, 0, 0, 0}, {0, 0, 1, 0, 0}};
check(s.minTotalDistance(g1) == 6, "示例1 -> 6");
// 单朋友
int[][] g2 = {{1}};
check(s.minTotalDistance(g2) == 0, "单朋友 [1] -> 0");
// 同列多朋友:[[1],[1],[1]] -> 选中间 (1,0),距离 0+1+1=2
int[][] g3 = {{1}, {1}, {1}};
check(s.minTotalDistance(g3) == 2, "竖排三友 -> 2");
// 暴力对照
check(s.minTotalDistanceBrute(g1) == 6, "暴力 示例1 -> 6");
check(s.minTotalDistanceBrute(g3) == 2, "暴力 竖排 -> 2");
// 混合:朋友 (0,0),(1,2),(3,0):行 [0,1,3] 中位 1,列 [0,2,0] 中位 0
// 到 (1,0):1 + 2 + 2 = 5;到 (1,1):1+1+0+2+1=5... 最小 5
int[][] g4 = {{1, 0, 0}, {0, 0, 1}, {0, 0, 0}, {1, 0, 0}};
check(s.minTotalDistance(g4) == 5, "混合 -> 5");
check(s.minTotalDistanceBrute(g4) == 5, "暴力 混合 -> 5");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(mn log(mn))(排序),空间 O(F)(F 为朋友数);
解法二:时间 O(mn * F),空间 O(F) 或 O(1)。
297. Serialize and Deserialize Binary Tree(二叉树的序列化与反序列化)
题目描述
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的
数据存储在一个文件或者内存中,同时也可以通过网路传输到另一个计算机环境,采取相反方式
重构得到原数据。请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 /
反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串
反序列化为原始的树结构。提示:输入输出格式与 LeetCode 目前使用的方式一致(层次遍历
格式),但你并非必须采取这种方式,也可以采用其他方法。
- 输入 :
TreeNoderoot/Stringdata - 输出 :
String(序列化结果)/TreeNode(反序列化结果)
示例 1:
root = [1,2,3,null,null,4,5] -> 序列化 -> 反序列化 -> [1,2,3,null,null,4,5]
示例 2:
root = [] -> "" / "[]" 往返一致
约束:
- 树中节点数在 0, 10\^4 内;-1000 <=
Node.val<= 1000。
解题思路
前序 DFS("null" 占位,推荐)/ 层次 BFS(LeetCode 官网格式)
- 解法一(前序 DFS):serialize 用前序遍历把 null 记为 "null"、值之间用逗号连接;
deserialize 按逗号切分后用队列按前序顺序重建(读到 "null" 返回 null)。
序列化长度 O(n),重建严格按前序栈序进行,不需要额外标记。 - 解法二(层次 BFS + 占位符):与 LeetCode 官方输入输出格式完全一致。serialize
用队列做层次遍历,空节点写成 "null"(末尾连续 null 可省略但为稳妥保留);
deserialize 同样用队列:每次从队头取父节点,按顺序为它接上左右孩子("null"
表示空),空孩子不入队。该写法最接近题目示例格式。
为什么前序需要占位 null:只有把空指针也写入序列化,反序列化时才知道哪个节点没有孩子,
才能完整重建树结构;否则仅凭节点值无法区分"缺左孩子"与"缺右孩子"。
代码实现
java
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
public class Codec {
// ---------- 解法一:前序 DFS("null" 占位) ----------
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
serializeDfs(root, sb);
return sb.toString();
}
private void serializeDfs(TreeNode node, StringBuilder sb) {
if (node == null) {
sb.append("null,");
return;
}
sb.append(node.val).append(',');
serializeDfs(node.left, sb);
serializeDfs(node.right, sb);
}
public TreeNode deserialize(String data) {
Deque<String> q = new ArrayDeque<>(Arrays.asList(data.split(",")));
return deserializeDfs(q);
}
private TreeNode deserializeDfs(Deque<String> q) {
if (q.isEmpty()) {
return null;
}
String s = q.poll();
if (s.equals("null") || s.isEmpty()) {
return null;
}
TreeNode node = new TreeNode(Integer.parseInt(s));
node.left = deserializeDfs(q);
node.right = deserializeDfs(q);
return node;
}
// ---------- 解法二:层次 BFS(与 LeetCode 示例格式一致) ----------
public String serializeBfs(TreeNode root) {
if (root == null) {
return "[]";
}
StringBuilder sb = new StringBuilder("[");
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode node = q.poll();
if (node == null) {
sb.append("null,");
continue;
}
sb.append(node.val).append(',');
q.offer(node.left);
q.offer(node.right);
}
sb.setLength(sb.length() - 1); // 去掉末尾逗号
sb.append(']');
// 丢弃末尾的连续 ",null"(示例格式)
String s = sb.toString();
while (s.endsWith(",null]")) {
s = s.substring(0, s.length() - 6) + "]";
}
if (s.equals("[]")) {
return "[]";
}
// 也兼容 "1,2,3,null,null,4,5" 无括号形式
return s.substring(1, s.length() - 1);
}
public TreeNode deserializeBfs(String data) {
if (data == null || data.equals("[]")) {
return null;
}
String s = data.trim();
if (s.startsWith("[") && s.endsWith("]")) {
s = s.substring(1, s.length() - 1);
}
if (s.isEmpty()) {
return null;
}
String[] tokens = s.split(",");
Queue<TreeNode> q = new LinkedList<>();
TreeNode root = new TreeNode(Integer.parseInt(tokens[0]));
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < tokens.length) {
TreeNode parent = q.poll();
if (!tokens[i].equals("null")) {
parent.left = new TreeNode(Integer.parseInt(tokens[i]));
q.offer(parent.left);
}
i++;
if (i < tokens.length && !tokens[i].equals("null")) {
parent.right = new TreeNode(Integer.parseInt(tokens[i]));
q.offer(parent.right);
}
i++;
}
return root;
}
// ---------- 本地测试数据结构 ----------
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public static void main(String[] args) {
Codec codec = new Codec();
// 示例 1:root = [1,2,3,null,null,4,5]
TreeNode root1 = new TreeNode(1,
new TreeNode(2),
new TreeNode(3, new TreeNode(4), new TreeNode(5)));
String s1 = codec.serialize(root1);
TreeNode r1 = codec.deserialize(s1);
check(serializeLocal(r1).equals(serializeLocal(root1)), "示例1 前序往返一致");
check(codec.deserialize(codec.serialize(null)) == null, "空树 前序往返为 null");
// 示例 2:root = [](层次 BFS 往返)
String e2 = codec.serializeBfs(null);
check(e2.equals("[]"), "示例2 serializeBfs(null) -> []");
check(codec.deserializeBfs(e2) == null, "示例2 deserializeBfs([]) -> null");
// 示例 1 的层次 BFS 往返
String s1b = codec.serializeBfs(root1);
check(s1b.equals("1,2,3,null,null,4,5"), "示例1 BFS 序列化格式正确 -> " + s1b);
TreeNode r1b = codec.deserializeBfs(s1b);
check(serializeLocal(r1b).equals(serializeLocal(root1)), "示例1 BFS 往返一致");
// 单节点
TreeNode single = new TreeNode(7);
check(codec.deserialize(codec.serialize(single)).val == 7, "单节点前序往返");
check(codec.deserializeBfs(codec.serializeBfs(single)).val == 7, "单节点BFS往返");
// 只有左孩子(易错:层次格式 [1,2] 表示 1 左为 2 右为 null)
TreeNode leftOnly = new TreeNode(1, new TreeNode(2), null);
String lb = codec.serializeBfs(leftOnly);
check(lb.equals("1,2"), "左链 BFS -> 1,2");
check(serializeLocal(codec.deserializeBfs(lb)).equals(serializeLocal(leftOnly)), "左链 BFS 往返");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(n)(序列化字符串 + 递归栈 + 队列);
解法二:时间 O(n),空间 O(n)。
298. Binary Tree Longest Consecutive Sequence(二叉树最长连续序列)
题目描述
给你一棵指定的二叉树的根节点 root,请你计算其中最长连续序列路径的长度。
最长连续序列路径是依次递增 1 的路径。该路径,可以是从某个初始节点到树中任意节点,
通过「父 - 子」关系连接而产生的任意路径,且必须从父节点到子节点,反过来不可以。
- 输入 :
TreeNoderoot(树中节点数 0, 10\^4),
-10^9 <=Node.val<= 10^9) - 输出 :
int(最长连续递增序列路径的长度;树空时返回 0)
示例 1:
root = [1,null,3,2,4,null,null,null,5] -> 3(最长连续序列 3-4-5)
示例 2:
root = [2,null,3,2,null,1] -> 2(最长连续序列 2-3,注意不是 3-2-1,因为要递增)
约束:
- 节点数 0, 10^4;-10^9 <=
Node.val<= 10^9。
解题思路
自顶向下 DFS(传当前长度 + 父值,推荐)/ 自底向上返回子树内最长前缀
- 解法一(自顶向下 DFS):递归时传入父节点值与"截至父节点的连续长度 len。
若 child.val == parent.val + 1,则 len + 1,否则重新计为 1。
全局 ans 维护最大值。根节点视为起点,长度为 1(空树为 0)。实现直白且常数小。 - 解法二(自底向上 DFS):dfs(node) 返回"以 node 为起点、能向下延伸的最长连续递增
前缀长度"(children.val == node.val + 1 时接续,否则从 1 重新开始),
递归同时用节点值更新全局最大。两者等价,自底向上写法便于在子树粒度复用。
为什么只需要向下的方向:题目明确"必须从父节点到子节点",因此不存在"拐弯"(先下后上
再下)的路径,只需要在一条自上而下的祖先-后代链中取最长的一段。
为什么用 "parent + 1" 判断:定义是持续递增 1,因此只有恰好相差 1 才能接续;
等差为 1 之外(如 3 -> 5)即使递增也断链重计。
代码实现
java
public class Solution {
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
private int best;
public int longestConsecutive(TreeNode root) {
best = 0;
if (root == null) {
return 0;
}
longestFrom(root, root.val, 1);
return best;
}
private void longestFrom(TreeNode node, int parentVal, int len) {
if (node != null && node.val == parentVal + 1) {
len++; // 接续:值恰好大 1
}
if (node == null) {
return;
}
best = Math.max(best, len);
longestFrom(node.left, node.val, node.val == parentVal + 1 ? len : 1);
longestFrom(node.right, node.val, node.val == parentVal + 1 ? len : 1);
}
public int longestConsecutiveBottomUp(TreeNode root) {
best = 0;
dfsBottomUp(root);
return best;
}
private int dfsBottomUp(TreeNode node) {
if (node == null) {
return 0;
}
int leftLen = dfsBottomUp(node.left);
int rightLen = dfsBottomUp(node.right);
int cur = 1; // 以 node 为起点至少长度 1
if (node.left != null && node.left.val == node.val + 1) {
cur = Math.max(cur, 1 + leftLen);
}
if (node.right != null && node.right.val == node.val + 1) {
cur = Math.max(cur, 1 + rightLen);
}
best = Math.max(best, cur);
return cur;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1:root = [1,null,3,2,4,null,null,null,5] -> 3(3-4-5)
TreeNode t1 = new TreeNode(1,
null,
new TreeNode(3,
new TreeNode(2),
new TreeNode(4, null, new TreeNode(5))));
check(s.longestConsecutive(t1) == 3, "示例1 -> 3");
check(s.longestConsecutiveBottomUp(t1) == 3, "自底向上 示例1 -> 3");
// 示例 2:root = [2,null,3,2,null,1] -> 2(2-3)
TreeNode t2 = new TreeNode(2,
null,
new TreeNode(3, new TreeNode(2, null, new TreeNode(1)), null));
check(s.longestConsecutive(t2) == 2, "示例2 -> 2");
check(s.longestConsecutiveBottomUp(t2) == 2, "自底向上 示例2 -> 2");
// 空树
check(s.longestConsecutive(null) == 0, "空树 -> 0");
check(s.longestConsecutiveBottomUp(null) == 0, "自底向上 空树 -> 0");
// 单节点
check(s.longestConsecutive(new TreeNode(5)) == 1, "单节点 -> 1");
// 左链递增 1-2-3
TreeNode t3 = new TreeNode(1, new TreeNode(2, new TreeNode(3), null), null);
check(s.longestConsecutive(t3) == 3, "左链 1-2-3 -> 3");
// 需要"重新计数":1-2 断 3-4(1->3 不是递增1)
TreeNode t4 = new TreeNode(1, new TreeNode(2, new TreeNode(3, new TreeNode(4), null), null), null);
// 路径 1-2 长度2;2-3 长度2(2+1=3? 不对 2->3 递增1 接续:2 的长度为2,3 接 2 得 3;3->4 接续得 4
// 检查:1->2 递增1(len1+1=2);2->3 递增1(len2+1=3);3->4 递增1(len3+1=4)=> 最长 4
check(s.longestConsecutive(t4) == 4, "链式 1-2-3-4 -> 4");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(h)(递归深度,最坏 O(n));
解法二:时间 O(n),空间 O(h)。
299. Bulls and Cows(猜数字游戏)
题目描述
你在和朋友一起玩猜数字(Bulls and Cows)游戏,规则如下:写出一个秘密数字,
并请朋友猜这个数字是多少。朋友每猜测一次,你就会给他一个包含下述信息的提示:
- "Bulls"(公牛):猜测数字中有多少位属于数字和确切位置都猜对了;
- "Cows"(奶牛):有多少位属于数字猜对了但是位置不对,即非公牛数字可以通过重新排列
转换成公牛数字。
给你一个秘密数字 secret 和朋友猜测的数字 guess,请你返回对朋友这次猜测的提示。提示的
格式为 "xAyB",x是公牛个数,y是奶牛个数,A 表示公牛,B 表示奶牛。注意秘密数字和
猜测数字都可能含有重复数字。 - 输入 :
Stringsecret,Stringguess(1 <= secret.length, guess.length <= 1000,
两者长度相等且仅含数字 0-9) - 输出 :
String("xAyB" 格式)
示例 1:
secret = "1807", guess = "7810" -> "1A3B"(公牛 1 位:8;奶牛 3 位:7、0、1)
示例 2:
secret = "1123", guess = "0111" -> "1A1B"(公牛 1 位:下标 2 的 '1';奶牛 1 位)
约束:
- 1 <= secret.length, guess.length <= 1000;secret 与 guess 仅含数字且长度相等。
解题思路
两次扫描(公牛 + 频次,推荐)/ 同数组差频(单次遍历)
- 解法一(两遍扫描):第一遍统计公牛数 bulls:位置 i 上 secreti == guessi;
同时把 guess 中"非公牛"数字计数到 frequess,把 secret 中"非公牛"数字计数到
freqSecret。第二遍遍历 guess 的每个数字 c:若 freqSecretc > 0 则 cows++
并递减(一个数字至多配对一次)。最后返回 bulls + "A" + cows + "B"。 - 解法二(单数组差频):用一个 int10 diff;遍历 i,若 si==gi bulls++;
否则 si 对应格 +1(多余供应),gi 对应格 -1(需求),遍历结束后 cows =
所有正值的和(即 secret 中未被扣减的匹配候选);等价也可用 min(正和,负和绝对值)。
一次遍历完成,不会重复配对。
为什么需要频次而非集合:数字可重复(如 "1123" 与 "0111"),同一位数字可能出现多次,
必须按次数配对;用频次表保证每个数字最多被消费一次,防止把同一个字符计算两次奶牛。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
public String getHint(String secret, String guess) {
int[] freqSecret = new int[10];
int[] freqGuess = new int[10];
int bulls = 0;
for (int i = 0; i < secret.length(); i++) {
char s = secret.charAt(i);
char g = guess.charAt(i);
if (s == g) {
bulls++;
} else {
freqSecret[s - '0']++;
freqGuess[g - '0']++;
}
}
int cows = 0;
for (int d = 0; d < 10; d++) {
cows += Math.min(freqSecret[d], freqGuess[d]); // 每个数字最多配对 min 次
}
return bulls + "A" + cows + "B";
}
public String getHintOnce(String secret, String guess) {
int[] diff = new int[10];
int bulls = 0;
for (int i = 0; i < secret.length(); i++) {
int s = secret.charAt(i) - '0';
int g = guess.charAt(i) - '0';
if (s == g) {
bulls++;
} else {
diff[s]++; // secret 侧多出一个候选
diff[g]--; // guess 侧多出一个需求
}
}
int extra = 0; // secret 中未被消费的候选总数 = 可配对的奶牛数
for (int v : diff) {
if (v > 0) {
extra += v;
}
}
int cows = guess.length() - bulls - extra; // 总非公牛 - 未配对候选 = 已配对奶牛
return bulls + "A" + cows + "B";
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
check(s.getHint("1807", "7810").equals("1A3B"), "示例1 -> 1A3B");
// 示例 2
check(s.getHint("1123", "0111").equals("1A1B"), "示例2 -> 1A1B");
// 完全相同
check(s.getHint("1234", "1234").equals("4A0B"), "全对 -> 4A0B");
// 全错但数字相同(错位)
check(s.getHint("12", "21").equals("0A2B"), "错位 -> 0A2B");
// 无任何匹配
check(s.getHint("1", "2").equals("0A0B"), "无匹配 -> 0A0B");
// 重复数字:secret=1122 guess=2211:0A4B?分析:公牛0;
// secret多余 1x2、guess需要 1x2(奶牛2),secret需要 2x2、guess多余 2x2(奶牛2)=> 0A4B
check(s.getHint("1122", "2211").equals("0A4B"), "1122/2211 -> 0A4B");
// 解法二抽查
check(s.getHintOnce("1807", "7810").equals("1A3B"), "单遍 示例1");
check(s.getHintOnce("1123", "0111").equals("1A1B"), "单遍 示例2");
check(s.getHintOnce("1122", "2211").equals("0A4B"), "单遍 1122/2211");
check(s.getHintOnce("1234", "1234").equals("4A0B"), "单遍 全对");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(10);
解法二:时间 O(n),空间 O(10)。
300. Longest Increasing Subsequence(最长递增子序列)
题目描述
给你一个整数数组 nums,找到其中最长严格递增子序列的长度。子序列是由数组派生
而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。
- 输入 :
int[]nums(1 <=nums.length<= 2500,-10^4 <=nums[i]<= 10^4) - 输出 :
int(最长严格递增子序列长度)
示例 1:
nums = [10,9,2,5,3,7,101,18] -> 4(最长递增子序列是 [2,3,7,101])
示例 2:
nums = [0,1,0,3,2,3] -> 4([0,1,2,3] 或 [0,1,3] 等)
示例 3:
nums = [7,7,7,7,7,7,7] -> 1(严格递增,相等不能接续)
约束:
- 1 <=
nums.length<= 2500;-10^4 <=nums[i]<= 10^4。
解题思路
DP(O(n^2))/ 贪心 + 二分(O(n log n),推荐)
- 解法一(常规 DP):dpi 表示以 numsi 结尾的最长严格递增子序列长度,
dpi = max(dpj + 1) 其中 j < i 且 numsj < numsi,初值全 1。
时间 O(n^2),直观易写,适合作为基础解法。 - 解法二(贪心 + 二分,推荐):维护 tails 数组,tailsk 表示长度为 k+1 的严格递增
子序列的最小末尾值。遍历 nums:二分找第一个 >= x 的位置(严格递增用 lower_bound),
若找到则更新 tailspos = x(同等长度保持末尾更小更优),否则追加 x。
最终 tails 的长度即答案。时间 O(n log n)。
为什么贪心正确:对于相同长度的递增子序列,末尾越小越有可能接上更长的后续元素,
因此用小值替换大值的"单调栈贪心"能保证任意时刻 tails 是长度固定的子序列中的最小尾部,
且 tails 本身单调递增(保证可以二分)。
代码实现
java
import java.util.Arrays;
public class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int ans = 0;
for (int i = 0; i < n; i++) {
dp[i] = 1; // 只含自身
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) { // 严格递增才允许接续
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
ans = Math.max(ans, dp[i]);
}
return ans;
}
public int lengthOfLISBinary(int[] nums) {
int[] tails = new int[nums.length];
int len = 0; // tails 有效长度
for (int x : nums) {
int lo = 0, hi = len; // 在 [0, len) 中找第一个 >= x 的位置
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (tails[mid] < x) {
lo = mid + 1;
} else {
hi = mid;
}
}
tails[lo] = x; // 替换(lo == 0 也无妨)或追加
if (lo == len) {
len++;
}
}
return len;
}
public static void main(String[] args) {
Solution s = new Solution();
// 示例 1
check(s.lengthOfLIS(new int[]{10, 9, 2, 5, 3, 7, 101, 18}) == 4, "示例1 -> 4");
// 示例 2
check(s.lengthOfLIS(new int[]{0, 1, 0, 3, 2, 3}) == 4, "示例2 -> 4");
// 示例 3:全相等
check(s.lengthOfLIS(new int[]{7, 7, 7, 7, 7, 7, 7}) == 1, "示例3 -> 1");
// 单元素
check(s.lengthOfLIS(new int[]{1}) == 1, "单元素 -> 1");
// 严格递增整段
check(s.lengthOfLIS(new int[]{1, 2, 3, 4, 5}) == 5, "全递增 -> 5");
// 严格递减
check(s.lengthOfLIS(new int[]{5, 4, 3, 2, 1}) == 1, "全递减 -> 1");
// 解法二抽查(与 DP 交叉验证多组)
int[][] cases = {
{10, 9, 2, 5, 3, 7, 101, 18},
{0, 1, 0, 3, 2, 3},
{7, 7, 7, 7, 7, 7, 7},
{1},
{1, 2, 3, 4, 5},
{5, 4, 3, 2, 1},
{3, 1, 4, 1, 5, 9, 2, 6, 5},
};
for (int[] c : cases) {
check(s.lengthOfLIS(c) == s.lengthOfLISBinary(c), "交叉验证 " + Arrays.toString(c));
}
check(true, "解法一/二在 7 组用例上结果一致");
System.out.println("全部测试通过");
}
private static void check(boolean ok, String name) {
if (!ok) {
System.out.println("FAIL | " + name);
System.exit(1);
}
System.out.println("PASS | " + name);
}
}
复杂度分析
解法一:时间 O(n^2),空间 O(n);
解法二:时间 O(n log n),空间 O(n)。