LeetCode 226-250 题解合集
文章目录
- [LeetCode 226-250 题解合集](#LeetCode 226-250 题解合集)
-
- [226. Invert Binary Tree(翻转二叉树)](#226. Invert Binary Tree(翻转二叉树))
- [227. Basic Calculator II(基本计算器 II)](#227. Basic Calculator II(基本计算器 II))
- [228. Summary Ranges(汇总区间)](#228. Summary Ranges(汇总区间))
- [229. Majority Element II(多数元素 II)](#229. Majority Element II(多数元素 II))
- [230. Kth Smallest Element in a BST(二叉搜索树中第K小的元素)](#230. Kth Smallest Element in a BST(二叉搜索树中第K小的元素))
- [231. Power of Two(2 的幂)](#231. Power of Two(2 的幂))
- [232. Implement Queue using Stacks(用栈实现队列)](#232. Implement Queue using Stacks(用栈实现队列))
- [233. Number of Digit One(数字 1 的个数)](#233. Number of Digit One(数字 1 的个数))
- [234. Palindrome Linked List(回文链表)](#234. Palindrome Linked List(回文链表))
- [235. Lowest Common Ancestor of a Binary Search Tree](#235. Lowest Common Ancestor of a Binary Search Tree)
- [236. Lowest Common Ancestor of a Binary Tree(二叉树的最近公共祖先)](#236. Lowest Common Ancestor of a Binary Tree(二叉树的最近公共祖先))
- [237. Delete Node in a Linked List(删除链表中的节点)](#237. Delete Node in a Linked List(删除链表中的节点))
- [238. Product of Array Except Self(除自身以外数组的乘积)](#238. Product of Array Except Self(除自身以外数组的乘积))
- [239. Sliding Window Maximum(滑动窗口最大值)](#239. Sliding Window Maximum(滑动窗口最大值))
- [240. Search a 2D Matrix II(搜索二维矩阵 II)](#240. Search a 2D Matrix II(搜索二维矩阵 II))
- [241. Different Ways to Add Parentheses(为运算表达式设计优先级)](#241. Different Ways to Add Parentheses(为运算表达式设计优先级))
- [242. Valid Anagram(有效的字母异位词)](#242. Valid Anagram(有效的字母异位词))
- [243. Shortest Word Distance(最短单词距离)](#243. Shortest Word Distance(最短单词距离))
- [244. Shortest Word Distance II(最短单词距离 II)](#244. Shortest Word Distance II(最短单词距离 II))
- [245. Shortest Word Distance III(最短单词距离 III)](#245. Shortest Word Distance III(最短单词距离 III))
- [246. Strobogrammatic Number(中心对称数)](#246. Strobogrammatic Number(中心对称数))
- [247. Strobogrammatic Number II(中心对称数 II)](#247. Strobogrammatic Number II(中心对称数 II))
- [248. Strobogrammatic Number III(中心对称数 III)](#248. Strobogrammatic Number III(中心对称数 III))
- [249. Group Shifted Strings(移位字符串分组)](#249. Group Shifted Strings(移位字符串分组))
- [250. Count Univalue Subtrees(统计同值子树)](#250. Count Univalue Subtrees(统计同值子树))
226. Invert Binary Tree(翻转二叉树)
题目描述
给你一棵二叉树的根节点 root,翻转这棵二叉树,并返回其根节点。
(把每个节点的左右子树互换)
- 输入 :
TreeNoderoot(树中节点数目范围在 0, 100 内,-100 <=Node.val<= 100) - 输出 :
TreeNode(翻转后的根节点)
示例 1:
root=[4,2,7,1,3,6,9] -> [4,7,2,9,6,3,1]
示例 2:
root=[2,1,3] -> [2,3,1]
示例 3:
root=[] -> []
约束:
- 树中节点数目范围在 0, 100 内;-100 <=
Node.val<= 100。
解题思路
自顶向下交换 / 递归 / 层序迭代
- 解法一(递归 DFS):交换当前节点的左右子树,再递归翻转左右子树。
- 解法二(迭代 BFS 层序):用队列逐层访问,每个节点交换左右孩子再入队。
- 解法三(前序/后序栈):与 BFS 等价,用栈遍历。
为什么交换后各自子树仍需翻转:子树内部的结构也要镜像,只交换根节点左右孩子
无法翻转更深层结构。
代码实现
java
public class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
TreeNode tmp = root.left; // 交换左右孩子
root.left = root.right;
root.right = tmp;
invertTree(root.left); // 递归翻转左右子树
invertTree(root.right);
return root;
}
public TreeNode invertTreeBFS(TreeNode root) {
if (root == null) return null;
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
TreeNode tmp = cur.left;
cur.left = cur.right;
cur.right = tmp;
if (cur.left != null) q.offer(cur.left);
if (cur.right != null) q.offer(cur.right);
}
return root;
}
public TreeNode invertTreeStack(TreeNode root) {
if (root == null) return null;
java.util.Deque<TreeNode> st = new java.util.ArrayDeque<>();
st.push(root);
while (!st.isEmpty()) {
TreeNode cur = st.pop();
TreeNode tmp = cur.left;
cur.left = cur.right;
cur.right = tmp;
if (cur.left != null) st.push(cur.left);
if (cur.right != null) st.push(cur.right);
}
return root;
}
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) {
Solution s = new Solution();
check(serialize(s.invertTree(build(new Integer[]{4,2,7,1,3,6,9}))), "[4,7,2,9,6,3,1]", "示例1");
check(serialize(s.invertTree(build(new Integer[]{2,1,3}))), "[2,3,1]", "示例2");
check(serialize(s.invertTree(build(new Integer[]{}))), "[]", "示例3");
check(serialize(s.invertTreeBFS(build(new Integer[]{4,2,7,1,3,6,9}))), "[4,7,2,9,6,3,1]", "BFS示例1");
check(serialize(s.invertTreeBFS(build(new Integer[]{2,1,3}))), "[2,3,1]", "BFS示例2");
check(serialize(s.invertTreeStack(build(new Integer[]{4,2,7,1,3,6,9}))), "[4,7,2,9,6,3,1]", "栈示例1");
check(serialize(s.invertTreeStack(build(new Integer[]{2,1,3}))), "[2,3,1]", "栈示例2");
}
private static void check(String actual, String expected, String name) {
System.out.println((expected.equals(actual) ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (!expected.equals(actual)) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
三种解法时间均 O(n),空间:递归 O(h)(h 为树高),BFS O(w)(最宽层),栈 O(h)。
227. Basic Calculator II(基本计算器 II)
题目描述
给你一个字符串表达式 s,请你实现一个基本计算器来计算并返回它的值。
整数除法仅保留整数部分。注意:不允许使用任何将字符串作为数学表达式计算的内置函数,
比如 eval()。
- 输入 :
Strings(1 <=s.length<= 3 * 10^5,s由整数和算符 ('+','-','*','/') 组成,
中间由一些空格隔开,不含括号) - 输出 :
int(表达式的值,保证结果在 32 位整数范围内)
示例 1:
s="3+2*2" -> 7
示例 2:
s=" 3/2 " -> 1
示例 3:
s=" 3+5 / 2 " -> 5
约束:
- 1 <=
s.length<= 3 * 10^5;s由整数和算符 ('+','-','*','/') 组成;不含括号。
解题思路
栈 + 延迟运算(在处理运算符时触发计算)
乘除优先级高于加减,因此不能从左到右直接累加。经典做法:
扫描时维护两个信息:上一运算符 op 与上一操作数 num。遇到新运算符时结算:
- 若 op 为 '+',num 入栈(正数);
- 若 op 为 '-',-num 入栈;
- 若 op 为 '*',栈顶 *= num(立即乘);
- 若 op 为 '/',栈顶 /= num。
遍历结束后栈内元素之和即结果(所有加减已被拆成 正/负 数,乘除已合并进栈顶)。
为什么能这样:把表达式改写成"若干个带符号数字相加",乘除在遇到下一运算符时
立刻作用于栈顶,天然满足优先级。
代码实现
java
public class Solution {
public int calculate(String s) {
java.util.Deque<Integer> stack = new java.util.ArrayDeque<>();
int num = 0;
char op = '+'; // 前一个运算符,初始为 '+'
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
num = num * 10 + (c - '0');
}
// 当前是运算符或末尾:结算之前的 num
if ((!Character.isDigit(c) && c != ' ') || i == s.length() - 1) {
switch (op) {
case '+': stack.push(num); break;
case '-': stack.push(-num); break;
case '*': stack.push(stack.pop() * num); break;
case '/': stack.push(stack.pop() / num); break;
}
op = c; // 更新运算符
num = 0;
}
}
int res = 0;
for (int v : stack) res += v; // 栈内正负数之和
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.calculate("3+2*2"), 7, "示例1");
check(s.calculate(" 3/2 "), 1, "示例2");
check(s.calculate(" 3+5 / 2 "), 5, "示例3");
check(s.calculate("1-1+1"), 1, "加减连续");
check(s.calculate("42"), 42, "单数");
check(s.calculate(" 12 / 4 * 3 "), 9, "乘除连续");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
时间 O(n),空间 O(n)(栈中元素不超过操作数个数)。
228. Summary Ranges(汇总区间)
题目描述
给定一个无重复元素的有序整数数组 nums。
返回恰好覆盖数组中所有数字的最小有序区间范围列表。
也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于
nums 的数字 x。列表中的每个区间范围 `a`,`b` 应该按如下格式输出:
- "
a->b",如果a!=b; - "
a",如果a==b。 - 输入 :
int[]nums(0 <=nums.length<= 20,-2^31 <=nums[i]<= 2^31 - 1) - 输出 :
List<String>(区间范围列表)
示例 1:
nums=[0,1,2,4,5,7] -> ["0->2","4->5","7"]
示例 2:
nums=[0,2,3,4,6,8,9] -> ["0","2->4","6","8->9"]
约束:
- 0 <=
nums.length<= 20;-2^31 <=nums[i]<= 2^31 - 1;nums中的所有值都互不相同,且按递增顺序排列。
解题思路
线性扫描合并连续段
维护"当前区间起点 start",从下标 1 开始:若 numsi != numsi-1 + 1,
说明连续性断开,把 start, i-1 收尾成字符串,并更新 start = i。
- 解法一(快照式循环):一次遍历,必要时使用 numsi - 1 == numsi-1 判断连续。
- 解法二(内层 while 扩展区间):每次从 start 出发 while 连续推进 end,
一段区间处理完立即输出;思路等价但结构更直观。
为什么 O(n) 足够:数组有序无重复,连续性只由相邻元素决定,单趟扫描即可。
代码实现
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Solution {
public List<String> summaryRanges(int[] nums) {
List<String> res = new ArrayList<>();
if (nums.length == 0) return res;
int start = nums[0];
for (int i = 1; i <= nums.length; i++) {
// 到达末尾 或 连续性断开
if (i == nums.length || nums[i] != nums[i - 1] + 1) {
res.add(start == nums[i - 1] ? String.valueOf(start)
: start + "->" + nums[i - 1]);
if (i < nums.length) start = nums[i];
}
}
return res;
}
public List<String> summaryRangesWhile(int[] nums) {
List<String> res = new ArrayList<>();
int i = 0;
while (i < nums.length) {
int start = nums[i];
int j = i;
while (j + 1 < nums.length && nums[j + 1] == nums[j] + 1) {
j++;
}
res.add(start == nums[j] ? String.valueOf(start) : start + "->" + nums[j]);
i = j + 1;
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.summaryRanges(new int[]{0,1,2,4,5,7}), new String[]{"0->2","4->5","7"}, "示例1");
check(s.summaryRanges(new int[]{0,2,3,4,6,8,9}), new String[]{"0","2->4","6","8->9"}, "示例2");
check(s.summaryRanges(new int[]{}), new String[]{}, "空数组");
check(s.summaryRangesWhile(new int[]{0,1,2,4,5,7}), new String[]{"0->2","4->5","7"}, "while版示例1");
check(s.summaryRanges(new int[]{-2147483648,-2147483647,2147483647}), new String[]{"-2147483648->-2147483647","2147483647"}, "边界值");
}
private static void check(List<String> actual, String[] expected, String name) {
boolean ok = actual.size() == expected.length;
if (ok) {
for (int i = 0; i < actual.size(); i++) {
if (!actual.get(i).equals(expected[i])) { ok = false; break; }
}
}
System.out.println((ok ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + java.util.Arrays.toString(expected));
if (!ok) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
时间 O(n),空间 O(1)(不计输出列表)。
229. Majority Element II(多数元素 II)
题目描述
给定一个大小为 n 的整数数组,找出其中所有出现超过 ⌊n/3⌋ 次的元素。
- 输入 :
int[]nums(1 <=nums.length<= 5 * 104,-109 <=nums[i]<= 10^9) - 输出 :
List<Integer>(所有出现次数超过n/3 的元素)
示例 1:
nums=[3,2,3] -> [3]
示例 2:
nums=[1] -> [1]
示例 3:
nums=[1,2] -> [1,2]
约束:
- 1 <=
nums.length<= 5 * 104;-109 <=nums[i]<= 10^9。
解题思路
摩尔投票扩展(Boyer-Moore 多数投票 ≥ n/3 -> 至多两个候选)
出现超过 n/3 的元素至多 2 个。因此可以维护两个候选及其计数:
- 遍历:若 value 等于候选一/二则对应计数加一;
否则两个计数都大于 0 时都减一("抵消");
若某候选计数为 0,则把当前值设为新候选。 - 第一遍得到至多两个"候选项",第二遍统计真实出现次数,超过 n/3 才收录。
为什么需要第二遍验证:抵消过程只能保证"多数元素必在候选里",不能保证候选真的
超过 n/3,必须重新统计。 - 附加解法(HashMap 计数):直接统计所有元素频次再过滤,O(n) 空间,代码直观。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<Integer> majorityElement(int[] nums) {
// 第一趟:选出至多两个候选
int cand1 = 0, cand2 = 0, cnt1 = 0, cnt2 = 0;
for (int v : nums) {
if (cnt1 > 0 && v == cand1) {
cnt1++;
} else if (cnt2 > 0 && v == cand2) {
cnt2++;
} else if (cnt1 == 0) {
cand1 = v; cnt1 = 1;
} else if (cnt2 == 0) {
cand2 = v; cnt2 = 1;
} else {
cnt1--; cnt2--; // 三个不同元素互相抵消
}
}
// 第二趟:验证真实次数
List<Integer> res = new ArrayList<>();
int n = nums.length;
int c1 = 0, c2 = 0;
for (int v : nums) {
if (cnt1 > 0 && v == cand1) c1++;
else if (cnt2 > 0 && v == cand2) c2++;
}
if (cnt1 > 0 && c1 > n / 3) res.add(cand1);
if (cnt2 > 0 && c2 > n / 3) res.add(cand2);
return res;
}
public List<Integer> majorityElementMap(int[] nums) {
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
for (int v : nums) cnt.put(v, cnt.getOrDefault(v, 0) + 1);
List<Integer> res = new ArrayList<>();
for (java.util.Map.Entry<Integer, Integer> e : cnt.entrySet()) {
if (e.getValue() > nums.length / 3) res.add(e.getKey());
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.majorityElement(new int[]{3,2,3}), new int[]{3}, "示例1");
check(s.majorityElement(new int[]{1}), new int[]{1}, "示例2");
check(s.majorityElement(new int[]{1,2}), new int[]{1,2}, "示例3");
check(s.majorityElementMap(new int[]{3,2,3}), new int[]{3}, "哈希法示例1");
check(s.majorityElement(new int[]{1,1,1,2,2,3,3,3}), new int[]{1,3}, "两个候选");
check(s.majorityElement(new int[]{2,2,2,3,3,3,4}), new int[]{2,3}, "补充:双合格");
}
private static void check(List<Integer> actual, int[] expected, String name) {
java.util.Collections.sort(actual);
int[] e = expected.clone();
java.util.Arrays.sort(e);
boolean ok = actual.size() == e.length;
if (ok) {
for (int i = 0; i < e.length; i++) {
if (actual.get(i) != e[i]) { ok = false; break; }
}
}
System.out.println((ok ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + java.util.Arrays.toString(expected));
if (!ok) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
摩尔投票:时间 O(n),空间 O(1)。哈希法:时间 O(n),空间 O(n)。
230. Kth Smallest Element in a BST(二叉搜索树中第K小的元素)
题目描述
给定一个二叉搜索树的根节点 root,和一个整数 k,
请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
- 输入 :
TreeNoderoot,intk(树的节点数n,1 <=k<=n<= 10^4,0 <=Node.val<= 10^4) - 输出 :
int(BST 中第k小的值)
示例 1:
root=[3,1,4,null,2], k=1 -> 1
示例 2:
root=[5,3,6,2,4,null,null,1], k=3 -> 3
约束:
- 树的节点数
n;1 <=k<=n<= 10^4;0 <=Node.val<= 10^4。
进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第k小的值,
你将如何优化算法?
解题思路
中序遍历第 k 个 / 迭代栈 / 计数子树(进阶)
BST 的中序遍历结果是升序序列,第 k 小即中序第 k 个访问到的节点。
- 解法一(递归中序 + 计数):left -> 自己 -> right 的顺序遍历,计数到 k 即返回。
- 解法二(显式栈迭代):等价中序,空间 O(h),便于 early-stop。
- 进阶优化(计数节点子树):给每个节点记录"左子树节点数",查找时比较 k 与左子树规模,
O(log n) 定位,但需要维护字段,本文件以中序为主并简单说明。
为什么中序即升序:BST 左子树所有值 < 根 < 右子树所有值,按"左-根-右"访问
恰好按从小到大输出。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
private int count = 0;
private int ans = 0;
public int kthSmallest(TreeNode root, int k) {
count = 0;
ans = 0;
inorder(root, k);
return ans;
}
private void inorder(TreeNode node, int k) {
if (node == null || count >= k) return;
inorder(node.left, k); // 左子树全部比根小
if (++count == k) { // 访问到第 k 个
ans = node.val;
return;
}
inorder(node.right, k);
}
public int kthSmallestStack(TreeNode root, int k) {
java.util.Deque<TreeNode> stack = new java.util.ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) { // 一路向左压栈
stack.push(cur);
cur = cur.left;
}
cur = stack.pop(); // 弹栈即访问
if (--k == 0) return cur.val; // 第 k 个
cur = cur.right; // 转向右子树
}
return -1; // 不会到达
}
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) {
Solution s = new Solution();
check(s.kthSmallest(build(new Integer[]{3,1,4,null,2}), 1), 1, "示例1");
check(s.kthSmallest(build(new Integer[]{5,3,6,2,4,null,null,1}), 3), 3, "示例2");
check(s.kthSmallestStack(build(new Integer[]{3,1,4,null,2}), 1), 1, "栈法示例1");
check(s.kthSmallestStack(build(new Integer[]{5,3,6,2,4,null,null,1}), 3), 3, "栈法示例2");
check(s.kthSmallest(build(new Integer[]{1}), 1), 1, "单节点");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
解法一/二:时间 O(h + k)(h 为树高),空间 O(h)。
231. Power of Two(2 的幂)
题目描述
给你一个整数 n,请你判断该整数是否是 2 的幂次方。
如果是,返回 true;否则,返回 false。
如果存在一个整数 x 使得 n == 2^x,则认为 n 是 2 的幂次方。
- 输入 :
intn(-2^31 <=n<= 2^31 - 1) - 输出 :
boolean(是否为 2 的幂)
示例 1:
n=1 -> true
示例 2:
n=16 -> true
示例 3:
n=3 -> false
示例 4:
n=4 -> true
示例 5:
n=5 -> false
约束:
- -2^31 <=
n<= 2^31 - 1。
解题思路
n & (n-1) 清零低位 / 位运算
- 解法一(n>0 且 n & (n-1) == 0):2 的幂的二进制只有一个 1;
n-1 会把唯一的 1 及其后所有位变为 1,按位与为 0。 - 解法二(n>0 且 n & (-n) == n):lowbit 等于自身(只保留最低位的 1)。
- 解法三(循环除 2):一直除以 2,最后到 1 则是;O(log n),直观。
为什么 n > 0 是前提:负数和 0 的二进制不含单个 1(负数补码有多个 1)。
代码实现
java
public class Solution {
public boolean isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
public boolean isPowerOfTwoLowbit(int n) {
return n > 0 && (n & -n) == n;
}
public boolean isPowerOfTwoLoop(int n) {
if (n <= 0) return false;
while (n % 2 == 0) {
n /= 2;
}
return n == 1;
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.isPowerOfTwo(1), true, "示例1");
check(s.isPowerOfTwo(16), true, "示例2");
check(s.isPowerOfTwo(3), false, "示例3");
check(s.isPowerOfTwo(4), true, "示例4");
check(s.isPowerOfTwo(5), false, "示例5");
check(s.isPowerOfTwoLowbit(16), true, "lowbit法");
check(s.isPowerOfTwoLoop(16), true, "循环法");
check(s.isPowerOfTwo(0), false, "0");
check(s.isPowerOfTwo(-16), false, "负数");
}
private static void check(boolean actual, boolean expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
解法一/二:时间 O(1),空间 O(1)。
解法三:时间 O(log n),空间 O(1)。
232. Implement Queue using Stacks(用栈实现队列)
题目描述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作
(push、pop、peek、empty)。
实现 MyQueue 类:
voidpush(intx) 将元素x推到队列的末尾;intpop()从队列的开头移除并返回元素;intpeek()返回队列开头的元素;booleanempty()如果队列为空,返回true;否则返回false。
说明:只能使用标准的栈操作(也就是pushtotop、peek/popfromtop、size和 isempty)。- 输入 :方法调用(1 <=
x<= 9,最多调用 100 次push、pop、peek和empty) - 输出 :各方法返回值
示例:MyQueue myQueue = new MyQueue();
myQueue.push(1); myQueue.push(2);
myQueue.peek(); // 1
myQueue.pop(); // 1
myQueue.empty(); // false
约束: - 1 <=
x<= 9;最多调用 100 次push、pop、peek和empty;每次调用pop和peek都能保证队列非空。
解题思路
双栈:入队栈 + 出队栈(摊还 O(1))
- 入队:直接压入 in 栈(元素按时间逆序叠放);
- 出队/取队头:若 out 栈为空,把 in 栈全部弹出压入 out(此时 out 顶即最早入队元素),
再从 out 弹出/查看。
为什么摊还 O(1):每个元素恰好被"从 in 弹出、压入 out、从 out 弹出"一次,
n 次操作总移动 O(n)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
public class MyQueue {
private Deque<Integer> in = new ArrayDeque<>();
private Deque<Integer> out = new ArrayDeque<>();
public void push(int x) {
in.push(x);
}
private void transfer() {
if (out.isEmpty()) {
while (!in.isEmpty()) {
out.push(in.pop()); // 反转顺序,栈顶即最早入队元素
}
}
}
public int pop() {
transfer();
return out.pop();
}
public int peek() {
transfer();
return out.peek();
}
public boolean empty() {
return in.isEmpty() && out.isEmpty();
}
public static void main(String[] args) {
MyQueue q = new MyQueue();
q.push(1);
q.push(2);
check(q.peek(), 1, "peek()");
check(q.pop(), 1, "pop()");
check(q.empty(), false, "empty()");
q.push(3);
check(q.pop(), 2, "pop() 2");
check(q.pop(), 3, "pop() 3");
check(q.empty(), true, "empty() 空");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
private static void check(boolean actual, boolean expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
push O(1);pop/peek 摊还 O(1)(最坏 O(n) 发生在转移那一刻);空间 O(n)。
233. Number of Digit One(数字 1 的个数)
题目描述
给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。
- 输入 :
intn(0 <=n<= 10^9) - 输出 :
int(1 出现的总次数)
示例 1:
n=13 -> 6(1,10,11,12,13 中 1 出现 6 次)
示例 2:
n=0 -> 0
约束:
- 0 <=
n<= 10^9。
解题思路
逐位统计(数学计数)
对十进制每一位(个、十、百...),统计"该位上是 1 的数字个数":
设当前位权重 w = 10^i,高位 high = n / (w*10),低位 low = n % w,当前位 cur = (n / w) % 10。
- cur == 0:该位 1 出现 high * w 次;
- cur == 1:high * w + (low + 1) 次;
- cur >= 2:(high + 1) * w 次。
推导依据:把 n 看成 high | cur | low,枚举 high 从 0 到 high-1 时该位固定为 1
有 w 种低频组合;cur==1 时 low 可 0...low(low+1 种);cur>=2 时 low 全可(w 种)。 - 附加解法(数位 DP):记忆化搜索,通用性强,适合 n 更大或统计其他数字,
实现略重,但作为对比写出数位 DP 版本。
为什么逐位累加不重不漏:每个数的每一位各被统计一次,按位独立互斥。
代码实现
java
public class Solution {
public int countDigitOne(int n) {
int count = 0;
for (long w = 1; w <= n; w *= 10) {
long high = n / (w * 10);
long cur = (n / w) % 10;
long low = n % w;
if (cur == 0) {
count += high * w;
} else if (cur == 1) {
count += high * w + low + 1;
} else {
count += (high + 1) * w;
}
}
return count;
}
private char[] digits;
private int[][] memo;
public int countDigitOneDP(int n) {
digits = String.valueOf(n).toCharArray();
memo = new int[digits.length][digits.length + 1];
for (int[] row : memo) java.util.Arrays.fill(row, -1);
return dfs(0, 0, true);
}
private int dfs(int pos, int cnt, boolean limited) {
if (pos == digits.length) return cnt;
if (!limited && memo[pos][cnt] != -1) return memo[pos][cnt];
int bound = limited ? digits[pos] - '0' : 9;
int sum = 0;
for (int d = 0; d <= bound; d++) {
sum += dfs(pos + 1, cnt + (d == 1 ? 1 : 0), limited && d == bound);
}
if (!limited) memo[pos][cnt] = sum;
return sum;
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.countDigitOne(13), 6, "示例1");
check(s.countDigitOne(0), 0, "示例2");
check(s.countDigitOneDP(13), 6, "数位DP示例1");
check(s.countDigitOneDP(0), 0, "数位DP示例2");
check(s.countDigitOne(99), 20, "补充:1..99共20个1");
check(s.countDigitOne(100), 21, "补充:1..100");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
逐位法:时间 O(log10 n),空间 O(1)。
数位 DP:时间 O(位数 * 11),空间 O(位数)。
234. Palindrome Linked List(回文链表)
题目描述
给你一个单链表的头节点 head,请你判断该链表是否为回文链表。
如果是,返回 true;否则,返回 false。
- 输入 :
ListNodehead(链表中节点数目在范围 1, 10\^5 内,0 <=Node.val<= 9) - 输出 :
boolean(是否为回文)
示例 1:
head=[1,2,2,1] -> true
示例 2:
head=[1,2] -> false
约束:
- 链表中节点数目在范围 1, 10\^5 内;0 <=
Node.val<= 9。
进阶:你能否用O(n)时间复杂度和O(1)空间复杂度解决此题?
解题思路
快慢指针找中点 + 反转后半段
- 解法一(快慢指针 + 反转后半):快指针走两步、慢指针走一步,慢指针停在(左)中点;
反转后半段链表,然后双指针从两端向中间比较,最后(可选)恢复链表。
空间 O(1),满足进阶要求。 - 解法二(复制到数组 + 双指针):先拷贝全部值到数组,从两端向中间比较。
空间 O(n),实现最直观,也可作为基准验证。
为什么反转后半不破坏前半:前半段起点 head 不动,后半段起点是反转后的新头;
两段等长(奇数时中点归属前半,忽略)。
代码实现
java
public class Solution {
public boolean isPalindrome(ListNode head) {
// 1) 快慢指针找中点
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2) 反转后半段(从 slow 开始的链表)
ListNode second = reverse(slow);
ListNode first = head;
// 3) 逐节点比较
while (second != null) {
if (first.val != second.val) {
return false;
}
first = first.next;
second = second.next;
}
return true;
}
private ListNode reverse(ListNode node) {
ListNode prev = null;
while (node != null) {
ListNode next = node.next;
node.next = prev;
prev = node;
node = next;
}
return prev;
}
public boolean isPalindromeArray(ListNode head) {
java.util.List<Integer> vals = new java.util.ArrayList<>();
while (head != null) {
vals.add(head.val);
head = head.next;
}
int i = 0, j = vals.size() - 1;
while (i < j) {
if (!vals.get(i).equals(vals.get(j))) return false;
i++;
j--;
}
return true;
}
static class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.isPalindrome(build(new int[]{1,2,2,1})), true, "示例1");
check(s.isPalindrome(build(new int[]{1,2})), false, "示例2");
check(s.isPalindrome(build(new int[]{1})), true, "单节点");
check(s.isPalindromeArray(build(new int[]{1,2,2,1})), true, "数组法示例1");
check(s.isPalindromeArray(build(new int[]{1,2})), false, "数组法示例2");
check(s.isPalindrome(build(new int[]{1,2,3,2,1})), true, "奇数长回文");
}
private static void check(boolean actual, boolean expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(1)。
解法二:时间 O(n),空间 O(n)。
235. Lowest Common Ancestor of a Binary Search Tree
题目描述
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:"对于有根树 T 的两个结点 p、q,
最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大
(一个节点也可以是它自己的祖先)。"
- 输入 :
TreeNoderoot,TreeNodep,TreeNodeq(树中节点数目在范围 2, 10\^5 内,
-10^9 <=Node.val<= 10^9,所有值互不相同,p!=q) - 输出 :
TreeNode(p和q的最近公共祖先)
示例 1:
root=[6,2,8,0,4,7,9,null,null,3,5], p=2, q=8 -> 6
示例 2:
root=[6,2,8,0,4,7,9,null,null,3,5], p=2, q=4 -> 2
示例 3:
root=[2,1], p=2, q=1 -> 2
约束:
- 所有
Node.val互不相同;p!=q;p和q均存在于给定的 BST 中。
解题思路
利用 BST 性质二分下沉
- 解法一(迭代):从根开始,若 p、q 都小于当前节点则向左;都大于则向右;
其他情况(p、q 分列两侧,或其一等于当前节点)当前节点即 LCA。 - 解法二(递归):同样逻辑递归实现。
为什么"分叉点即 LCA":若 p、q 一个在左子树一个在右子树,公共祖先只可能是
当前节点(再往下不可能同时包含两侧的值);若某节点等于 p 或 q,则它自身即 LCA。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode cur = root;
while (cur != null) {
if (p.val < cur.val && q.val < cur.val) {
cur = cur.left; // 都在左子树
} else if (p.val > cur.val && q.val > cur.val) {
cur = cur.right; // 都在右子树
} else {
return cur; // 分叉点或等于 p/q 之一
}
}
return null;
}
public TreeNode lowestCommonAncestorRec(TreeNode root, TreeNode p, TreeNode q) {
if (p.val < root.val && q.val < root.val) {
return lowestCommonAncestorRec(root.left, p, q);
}
if (p.val > root.val && q.val > root.val) {
return lowestCommonAncestorRec(root.right, p, q);
}
return root;
}
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
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) {
Solution s = new Solution();
TreeNode r1 = build(new Integer[]{6,2,8,0,4,7,9,null,null,3,5});
check(s.lowestCommonAncestor(r1, find(r1, 2), find(r1, 8)).val, 6, "示例1");
check(s.lowestCommonAncestor(r1, find(r1, 2), find(r1, 4)).val, 2, "示例2");
TreeNode r3 = build(new Integer[]{2,1});
check(s.lowestCommonAncestor(r3, find(r3, 2), find(r3, 1)).val, 2, "示例3");
check(s.lowestCommonAncestorRec(r1, find(r1, 2), find(r1, 4)).val, 2, "递归版");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
时间 O(h)(h 为树高,最坏 O(n) 退化链),空间 O(1)(迭代)/ O(h)(递归)。
236. Lowest Common Ancestor of a Binary Tree(二叉树的最近公共祖先)
题目描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:"对于有根树 T 的两个节点 p、q,
最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大
(一个节点也可以是它自己的祖先)。"
- 输入 :
TreeNoderoot,TreeNodep,TreeNodeq(树中节点数目在范围 2, 10\^5 内
-10^9 <=Node.val<= 10^9,所有Node.val互不相同,p!=q) - 输出 :
TreeNode(p和q的最近公共祖先)
示例 1:
root=[3,5,1,6,2,0,8,null,null,7,4], p=5, q=1 -> 3
示例 2:
root=[3,5,1,6,2,0,8,null,null,7,4], p=5, q=4 -> 5
示例 3:
root=[1,2], p=1, q=2 -> 1
约束:
- 树中节点数目在范围 2, 10\^5 内;-10^9 <=
Node.val<= 10^9;
所有Node.val互不相同;p!=q;p和q均存在于给定的二叉树中。
解题思路
递归后序"返回值"判定 / 记录父节点法
- 解法一(递归后序):从子树中找 p、q:
若左子树找到 p/q、右子树找到另一个,说明当前节点是 LCA;
若只在一侧找到,则把"找到的那个节点"向上返回;
若两边都是 null,返回 null。
为什么正确:LCA 是"分叉点",即 p、q 首次分属左右子树的那个节点;
若 p/q 是祖先关系,则返回先遇到的 p 或 q 本身。 - 解法二(哈希表记录父节点 + 集合追溯):BFS/DFS 一遍记录每个节点的父节点,
再从 p 出发沿父链把祖先全部加入 set,再沿 q 的父链找第一个出现在 set 中的节点。 - 解法三(路径法):找到 root->p 与 root->q 的路径,比较最后一个公共节点。
为什么解法一不需要额外数据结构:后序遍历天然"先处理子树再决定返回值",
每个节点只被访问一次。
代码实现
java
public class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root; // 空或命中的节点(自身可作祖先)
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) {
return root; // 左右各找到一个 => 当前节点是 LCA
}
return left != null ? left : right; // 只在一侧找到
}
public TreeNode lowestCommonAncestorParent(TreeNode root, TreeNode p, TreeNode q) {
// 第二遍 BFS 记录父节点
java.util.Map<TreeNode, TreeNode> parent = new java.util.HashMap<>();
java.util.Deque<TreeNode> stack = new java.util.ArrayDeque<>();
parent.put(root, null);
stack.push(root);
while (!parent.containsKey(p) || !parent.containsKey(q)) {
TreeNode cur = stack.pop();
if (cur.left != null) {
parent.put(cur.left, cur);
stack.push(cur.left);
}
if (cur.right != null) {
parent.put(cur.right, cur);
stack.push(cur.right);
}
}
java.util.Set<TreeNode> ancestors = new java.util.HashSet<>();
while (p != null) {
ancestors.add(p); // p 的祖链(含自己)
p = parent.get(p);
}
while (!ancestors.contains(q)) {
q = parent.get(q); // q 向上找第一个共同祖先
}
return q;
}
public TreeNode lowestCommonAncestorPath(TreeNode root, TreeNode p, TreeNode q) {
java.util.List<TreeNode> pathP = new java.util.ArrayList<>();
java.util.List<TreeNode> pathQ = new java.util.ArrayList<>();
if (!findPath(root, p, pathP) || !findPath(root, q, pathQ)) {
return null; // 题目保证存在,正常不会走到
}
TreeNode lca = root;
int i = 0;
while (i < pathP.size() && i < pathQ.size() && pathP.get(i) == pathQ.get(i)) {
lca = pathP.get(i);
i++;
}
return lca;
}
private boolean findPath(TreeNode root, TreeNode target, java.util.List<TreeNode> path) {
if (root == null) return false;
path.add(root);
if (root == target) return true;
if (findPath(root.left, target, path) || findPath(root.right, target, path)) {
return true;
}
path.remove(path.size() - 1); // 回溯
return false;
}
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
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) {
Solution s = new Solution();
TreeNode r1 = build(new Integer[]{3,5,1,6,2,0,8,null,null,7,4});
check(s.lowestCommonAncestor(r1, find(r1, 5), find(r1, 1)).val, 3, "示例1");
check(s.lowestCommonAncestor(r1, find(r1, 5), find(r1, 4)).val, 5, "示例2");
TreeNode r3 = build(new Integer[]{1,2});
check(s.lowestCommonAncestor(r3, find(r3, 1), find(r3, 2)).val, 1, "示例3");
check(s.lowestCommonAncestorParent(r1, find(r1, 5), find(r1, 4)).val, 5, "父节点法示例2");
check(s.lowestCommonAncestor(r1, find(r1, 6), find(r1, 4)).val, 5, "补充:同侧祖先");
check(s.lowestCommonAncestorPath(r1, find(r1, 5), find(r1, 1)).val, 3, "路径法示例1");
check(s.lowestCommonAncestorPath(r1, find(r1, 5), find(r1, 4)).val, 5, "路径法示例2");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(h)(递归栈深)。
解法二:时间 O(n),空间 O(n)。
解法三(路径法):时间 O(n),空间 O(h)。
237. Delete Node in a Linked List(删除链表中的节点)
题目描述
有一个单链表的 head,我们想删除其中某一个节点 node。
给你一个需要删除的节点 node。你将无法访问第一个节点 head。
链表所有值都是唯一的,并且给定的节点 node 不是链表中的最后一个节点。
删除给定的节点。注意,删除节点并不是指从内存中删除它,而是:
- 给定节点的值不应该存在于链表中;
- 链表节点数应该减少 1;
node前面的所有值顺序相同;node后面的所有值顺序相同。- 输入 :
ListNodenode(链表节点数目范围 2, 1000) - 输出 :
void(原地删除节点)
示例 1:
head=[4,5,1,9], node=5 -> [4,1,9]
示例 2:
head=[4,5,1,9], node=1 -> [4,5,9]
约束:
- 链表中所有值互不相同;给定的节点
node不是链表中的最后一个节点。
解题思路
"偷天换日":拿后继节点的值覆盖当前节点,再跳过后继
因为我们拿不到 head,无法知道前驱,也不能真正断开 node 的入边。
经典解法:
- 令 node.val = node.next.val(把后继的值复制过来);
- node.next = node.next.next(跳过真正的后继)。
效果:node 承载了原先"后继"的值,等价于删除了原 node(后续节点整体前移一位)。
为什么这是唯一可行解:无法访问前驱,只能"借尸还魂"。
为什么题目保证 node 不是尾节点:若 node 是尾节点,node.next 为空,
该技巧失效(需要前驱)。
代码实现
java
public class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val; // 用后继的值覆盖当前节点
node.next = node.next.next; // 删除后继
}
static class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
private static ListNode find(ListNode head, int val) {
while (head != null) {
if (head.val == val) return head;
head = head.next;
}
return null;
}
private static void check(int[] actual, int[] expected, String name) {
boolean ok = java.util.Arrays.equals(actual, expected);
System.out.println((ok ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + java.util.Arrays.toString(actual)
+ " expected=" + java.util.Arrays.toString(expected));
if (!ok) System.out.println(" <<< 断言失败: " + name);
}
public static void main(String[] args) {
Solution s = new Solution();
ListNode l1 = build(new int[]{4,5,1,9});
s.deleteNode(find(l1, 5));
check(toArr(l1), new int[]{4,1,9}, "示例1");
ListNode l2 = build(new int[]{4,5,1,9});
s.deleteNode(find(l2, 1));
check(toArr(l2), new int[]{4,5,9}, "示例2");
ListNode l3 = build(new int[]{1,2,3});
s.deleteNode(find(l3, 2));
check(toArr(l3), new int[]{1,3}, "补充:中间节点");
}
}
复杂度分析
时间 O(1),空间 O(1)。
238. Product of Array Except Self(除自身以外数组的乘积)
题目描述
给你一个整数数组 nums,返回数组 answer,其中 answeri 等于 nums 中
除 nums[i] 之外其余各元素的乘积。
题目数据保证数组 nums 之中任意元素的全部前缀元素和后缀的乘积都在 32 位整数范围内。
请不要使用除法,且在 O(n) 时间复杂度内完成此题。
- 输入 :
int[]nums(2 <=nums.length<= 10^5,-30 <=nums[i]<= 30) - 输出 :
int[](answer 数组)
示例 1:
nums=[1,2,3,4] -> [24,12,8,6]
示例 2:
nums=[-1,1,0,-3,3] -> [0,0,9,0,0]
约束:
- 2 <=
nums.length<= 10^5;-30 <=nums[i]<= 30;保证任意前缀/后缀乘积在 32 位整数范围内。
进阶:你可以在O(1)的额外空间复杂度内完成这个题目吗?(出于对空间复杂度分析的目的,
输出数组不被视为额外空间。)
解题思路
前缀积 × 后缀积
- 解法一(左右乘积数组):Li = nums0...i-1 的乘积,Ri = numsi+1...end 的乘积,
answeri = Li * Ri。空间 O(n)。 - 解法二(输出数组代替 L,滚动变量代替 R):第一趟从左往右把前缀积写入 answer;
第二趟从右往左用一个变量 R 累计后缀积,answeri *= R 后更新 R *= numsi。
空间 O(1)(除输出数组)。满足进阶要求。
为什么不能直接用总积除以自身:题目禁止除法,且有 0 时会出现 0/0 特判,
前后缀乘法天然规避。
代码实现
java
public class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
// 第一趟:ans[i] = 前缀积(nums[0..i-1])
ans[0] = 1;
for (int i = 1; i < n; i++) {
ans[i] = ans[i - 1] * nums[i - 1];
}
// 第二趟:R 从右往左累计后缀积
int R = 1;
for (int i = n - 1; i >= 0; i--) {
ans[i] *= R; // 前缀积 * 后缀积
R *= nums[i]; // 更新后缀积
}
return ans;
}
public int[] productExceptSelfLR(int[] nums) {
int n = nums.length;
int[] L = new int[n], R = new int[n], ans = new int[n];
L[0] = 1;
for (int i = 1; i < n; i++) L[i] = L[i - 1] * nums[i - 1];
R[n - 1] = 1;
for (int i = n - 2; i >= 0; i--) R[i] = R[i + 1] * nums[i + 1];
for (int i = 0; i < n; i++) ans[i] = L[i] * R[i];
return ans;
}
private static void check(int[] actual, int[] expected, String name) {
boolean ok = java.util.Arrays.equals(actual, expected);
System.out.println((ok ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + java.util.Arrays.toString(actual)
+ " expected=" + java.util.Arrays.toString(expected));
if (!ok) System.out.println(" <<< 断言失败: " + name);
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.productExceptSelf(new int[]{1,2,3,4}), new int[]{24,12,8,6}, "示例1");
check(s.productExceptSelf(new int[]{-1,1,0,-3,3}), new int[]{0,0,9,0,0}, "示例2");
check(s.productExceptSelfLR(new int[]{1,2,3,4}), new int[]{24,12,8,6}, "左右数组法示例1");
check(s.productExceptSelf(new int[]{5,6}), new int[]{6,5}, "双元素");
}
}
复杂度分析
解法一:时间 O(n),空间 O(n)。解法二:时间 O(n),空间 O(1)(不计输出)。
239. Sliding Window Maximum(滑动窗口最大值)
题目描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。
你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
- 输入 :
int[]nums,intk(1 <=nums.length<= 105,-104 <=nums[i]<= 10^4,1 <=k<=nums.length) - 输出 :
int[](每个滑动窗口中的最大值)
示例 1:
nums=[1,3,-1,-3,5,3,6,7], k=3 -> [3,3,5,5,6,7]
示例 2:
nums=[1], k=1 -> [1]
约束:
- 1 <=
nums.length<= 105;-104 <=nums[i]<= 10^4;1 <=k<=nums.length。
解题思路
单调队列(双端队列)/ 优先队列 / 分块法
- 解法一(单调递减双端队列):队首是窗口最大值。
窗口滑动时:
- 移除队首过期元素(下标 < i-k+1);
- 从队尾弹出所有值 <= 当前值的元素(它们不可能再成为窗口最大值);
- 当前元素下标入队尾;
- 窗口满后才输出队首。
单调性:队列中下标递增、值严格递减。
- 解法二(优先队列/大顶堆 + 懒删除):堆按值排序,元素存 (值, 下标),
取堆顶时把下标过期者弹出。时间 O(n log n)。 - 解法三(前缀/后缀分解分块):按 k 分成若干块,预处理每块前后缀最大值,
每个窗口 = 两块组合,O(n)。作为进阶参考。
为什么弹出队尾的小值安全:小值在当前窗口内永远不会成为最大值;
只要有大的新元素在后面,小值只可能被新元素"盖过",所以可以提前淘汰。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] ans = new int[n - k + 1];
Deque<Integer> dq = new ArrayDeque<>(); // 存下标,队首最大
for (int i = 0; i < n; i++) {
// 1) 移除窗口外的下标
while (!dq.isEmpty() && dq.peekFirst() < i - k + 1) {
dq.pollFirst();
}
// 2) 弹出所有值更小的队尾(它们不可能做最大值)
while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]) {
dq.pollLast();
}
// 3) 当前下标入队
dq.offerLast(i);
// 4) 窗口满,取队首
if (i >= k - 1) {
ans[i - k + 1] = nums[dq.peekFirst()];
}
}
return ans;
}
public int[] maxSlidingWindowHeap(int[] nums, int k) {
int n = nums.length;
int[] ans = new int[n - k + 1];
java.util.PriorityQueue<int[]> heap = new java.util.PriorityQueue<>((a, b) -> b[0] - a[0]);
for (int i = 0; i < n; i++) {
heap.offer(new int[]{nums[i], i});
if (i >= k - 1) {
// 弹出过期元素
while (heap.peek()[1] < i - k + 1) heap.poll();
ans[i - k + 1] = heap.peek()[0];
}
}
return ans;
}
public int[] maxSlidingWindowBlock(int[] nums, int k) {
int n = nums.length;
int[] ans = new int[n - k + 1];
// left[i]:当前块内从块首到 i 的最大值
int[] left = new int[n];
// right[i]:当前块内从 i 到块尾的最大值
int[] right = new int[n];
for (int i = 0; i < n; i++) {
if (i % k == 0) {
left[i] = nums[i]; // 块首
} else {
left[i] = Math.max(left[i - 1], nums[i]);
}
}
for (int i = n - 1; i >= 0; i--) {
if (i == n - 1 || (i + 1) % k == 0) {
right[i] = nums[i]; // 块尾
} else {
right[i] = Math.max(right[i + 1], nums[i]);
}
}
for (int i = 0; i + k - 1 < n; i++) {
// 窗口 [i, i+k-1] 的左半在 right,右半在 left
ans[i] = Math.max(right[i], left[i + k - 1]);
}
return ans;
}
private static void check(int[] actual, int[] expected, String name) {
boolean ok = java.util.Arrays.equals(actual, expected);
System.out.println((ok ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + java.util.Arrays.toString(actual)
+ " expected=" + java.util.Arrays.toString(expected));
if (!ok) System.out.println(" <<< 断言失败: " + name);
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.maxSlidingWindow(new int[]{1,3,-1,-3,5,3,6,7}, 3), new int[]{3,3,5,5,6,7}, "示例1");
check(s.maxSlidingWindow(new int[]{1}, 1), new int[]{1}, "示例2");
check(s.maxSlidingWindowHeap(new int[]{1,3,-1,-3,5,3,6,7}, 3), new int[]{3,3,5,5,6,7}, "堆法示例1");
check(s.maxSlidingWindowBlock(new int[]{1,3,-1,-3,5,3,6,7}, 3), new int[]{3,3,5,5,6,7}, "分块法示例1");
check(s.maxSlidingWindowBlock(new int[]{1}, 1), new int[]{1}, "分块法示例2");
check(s.maxSlidingWindow(new int[]{9,8,7,6,5}, 3), new int[]{9,8,7}, "降序窗口");
check(s.maxSlidingWindow(new int[]{1,3,1,2,0,5}, 3), new int[]{3,3,2,5}, "补充");
}
}
复杂度分析
解法一:每个元素进出队一次,总时间 O(n),空间 O(k)。
解法二:时间 O(n log n),空间 O(k)。
解法三(分块):时间 O(n),空间 O(n)。
240. Search a 2D Matrix II(搜索二维矩阵 II)
题目描述
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。
该矩阵具有以下特性:
- 每行的元素从左到右升序排列;
- 每列的元素从上到下升序排列。
- 输入 :
int[][]matrix,inttarget(m==matrix.length,n==matrix[i].length,
1 <= n, m <= 300,-10^9 <= matrix[i][j] <= 10^9) - 输出 :
boolean(是否存在目标值)
示例 1:
matrix=[[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]],
target=5 -> true
示例 2:
target=20 -> false
约束:
m==matrix.length;n==matrix[i].length;1 <=n,m<= 300;
-10^9 <=matrix[i][j]<= 10^9;每行的所有元素从左到右升序排列;每列的所有元素从上到下升序排列;
-10^9 <=target<= 10^9。
解题思路
右上角"阶梯搜索" / 逐行二分
- 解法一(右上角开始):从右上角 (0, n-1) 出发:
若 cur > target,则整列都更大,左移一列;
若 cur < target,则整行都更小,下移一行;
相等则命中。每一步都排除一整行或一整列,最多 m+n 步。
为什么这个点能用:右上角满足"同行左边都小、同列下边都大",两个方向唯一性。 - 解法二(逐行二分):对每行做二分 O(m log n)。
- 解法三(暴力):O(mn) 作为基准。
为什么不从左上角开始:左上角右下都更大,无法唯一决定排除方向。
代码实现
java
public class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int i = 0, j = n - 1; // 从右上角开始
while (i < m && j >= 0) {
int cur = matrix[i][j];
if (cur == target) {
return true;
} else if (cur > target) {
j--; // 当前列全部更大,左移
} else {
i++; // 当前行全部更小,下移
}
}
return false;
}
public boolean searchMatrixBinary(int[][] matrix, int target) {
for (int[] row : matrix) {
if (row.length > 0 && java.util.Arrays.binarySearch(row, target) >= 0) {
return true;
}
}
return false;
}
public boolean searchMatrixBrute(int[][] matrix, int target) {
for (int[] row : matrix) {
for (int v : row) {
if (v == target) return true;
}
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] m1 = {{1,4,7,11,15},{2,5,8,12,19},{3,6,9,16,22},{10,13,14,17,24},{18,21,23,26,30}};
check(s.searchMatrix(m1, 5), true, "示例1");
check(s.searchMatrix(m1, 20), false, "示例2");
check(s.searchMatrixBinary(m1, 5), true, "二分法示例1");
check(s.searchMatrixBrute(m1, 5), true, "暴力示例1");
check(s.searchMatrixBrute(m1, 20), false, "暴力示例2");
check(s.searchMatrix(m1, 30), true, "右下角边界");
check(s.searchMatrix(m1, 1), true, "左上角边界");
check(s.searchMatrix(new int[][]{{-5}}, -5), true, "单格");
}
private static void check(boolean actual, boolean expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
解法一:时间 O(m + n),空间 O(1)。
解法二:时间 O(m log n),空间 O(1)。
解法三(暴力):时间 O(mn),空间 O(1)。
241. Different Ways to Add Parentheses(为运算表达式设计优先级)
题目描述
给你一个由数字和运算符组成的字符串 expression,按不同优先级组合数字和运算符,
计算并返回所有可能组合的结果。你可以按任意顺序返回答案。
生成的测试用例满足其对应输出结果符合 32 位整数范围,不同结果的数量不超过 10^4。
- 输入 :
Stringexpression(1 <= expression.length <= 20) - 输出 :
List<Integer>(所有可能的计算结果)
示例 1:
expression="2-1-1" -> [0,2]
((2-1)-1 = 0;2-(1-1) = 2)
示例 2:
expression="2*3-4*5" -> [-34,-14,-10,-10,10]
2*(3-(4*5)) = -34;2*((3-4)*5) = -10;(2*(3-4))*5 = -10;
((2*3)-4)*5 = 10;(2*3)-(4*5) = -14
约束:
- 1 <= expression.length <= 20;expression 由数字和算符 ('+','-','*') 组成;
expression 中的所有整数互不相同(题意按数字与算符交替,无括号)。
解题思路
分治:以每个运算符为"最后执行"切分左右
任意加括号方式等价于"某个运算符最后做"。对表达式 expressionl...r:
- 枚举每个运算符位置 op:
左半 expressionl...op-1 的所有结果 lefts;
右半 expressionop+1...r 的所有结果 rights;
两两按 op 运算得到本层的所有结果。 - 若区间内没有运算符,则本身是一个数。
记忆化可避免重复计算;expression 长度 <= 20,直接递归也足够。
为什么按"最后运算符"分治不漏解:任何括号化方案最外层总有一个最后执行的
运算符,它把表达式切分成两个独立子问题,枚举它即枚举全部方案。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<Integer> diffWaysToCompute(String expression) {
return helper(expression, 0, expression.length() - 1);
}
private List<Integer> helper(String s, int l, int r) {
List<Integer> res = new ArrayList<>();
int num = 0;
boolean hasOp = false;
// 枚举"最后执行"的运算符
for (int i = l; i <= r; i++) {
char c = s.charAt(i);
if (c == '+' || c == '-' || c == '*') {
hasOp = true;
List<Integer> lefts = helper(s, l, i - 1);
List<Integer> rights = helper(s, i + 1, r);
for (int a : lefts) {
for (int b : rights) {
res.add(apply(c, a, b));
}
}
}
}
// 无运算符:纯数字区间
if (!hasOp) {
for (int i = l; i <= r; i++) num = num * 10 + (s.charAt(i) - '0');
res.add(num);
}
return res;
}
private int apply(char op, int a, int b) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
default: return a * b;
}
}
public static void main(String[] args) {
Solution s = new Solution();
check(equalSorted(s.diffWaysToCompute("2-1-1"), new ArrayList<>(java.util.Arrays.asList(0, 2))), true, "示例1");
check(equalSorted(s.diffWaysToCompute("2*3-4*5"), new ArrayList<>(java.util.Arrays.asList(-34,-14,-10,-10,10))), true, "示例2");
check(s.diffWaysToCompute("3").equals(java.util.Arrays.asList(3)), true, "单数字");
check(equalSorted(s.diffWaysToCompute("1+2*3"), new ArrayList<>(java.util.Arrays.asList(7, 9))), true, "补充:1+(2*3)=7,(1+2)*3=9");
}
private static void check(boolean ok, boolean expected, String name) {
System.out.println((expected == ok ? "PASS" : "FAIL") + " | " + name
+ " | ok=" + ok + " expected=" + expected);
if (expected != ok) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
时间 O(C(n))(卡特兰数级别,n 为算符个数,可以用记忆化解)
空间 O(C(n));n <= 20 时结果数 <= 10^4,安全。
242. Valid Anagram(有效的字母异位词)
题目描述
给定两个字符串 s 和 t,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
- 输入 :
Strings,Stringt(1 <=s.length,t.length<= 5 * 10^4) - 输出 :
boolean(是否为字母异位词)
示例 1:
s="anagram", t="nagaram" -> true
示例 2:
s="rat", t="car" -> false
约束:
s和t仅包含小写字母。
进阶:如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
解题思路
计数数组 / 排序 / 哈希表(unicode)
- 解法一(计数数组):题目限制小写字母,用 int26。遍历 s 加计数,
遍历 t 减计数,最后全为 0 则 true。时间 O(n),空间 O(1)。 - 解法二(排序比较):排序后相等即异位词,时间 O(n log n)。
- 进阶(HashMap 计数):字符是 unicode 时计数数组不再适用,改用
Map<Character, Integer>,逻辑相同但空间随字符种类变化。
为什么计数数组可行:小写字母只有 26 种,频率数组天然是"字符->次数"的映射,
最长两个字符串相同长度的前提下,频率一致等价于异位词。
代码实现
java
public class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
int[] cnt = new int[26];
for (char c : s.toCharArray()) cnt[c - 'a']++;
for (char c : t.toCharArray()) {
if (--cnt[c - 'a'] < 0) return false; // 频次不够
}
return true;
}
public boolean isAnagramSort(String s, String t) {
char[] a = s.toCharArray();
char[] b = t.toCharArray();
java.util.Arrays.sort(a);
java.util.Arrays.sort(b);
return java.util.Arrays.equals(a, b);
}
public boolean isAnagramUnicode(String s, String t) {
if (s.length() != t.length()) return false;
java.util.Map<Character, Integer> map = new java.util.HashMap<>();
for (char c : s.toCharArray()) map.put(c, map.getOrDefault(c, 0) + 1);
for (char c : t.toCharArray()) {
int v = map.getOrDefault(c, 0);
if (v == 0) return false;
map.put(c, v - 1);
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.isAnagram("anagram", "nagaram"), true, "示例1");
check(s.isAnagram("rat", "car"), false, "示例2");
check(s.isAnagramSort("anagram", "nagaram"), true, "排序法示例1");
check(s.isAnagramSort("rat", "car"), false, "排序法示例2");
check(s.isAnagramUnicode("anagram", "nagaram"), true, "unicode版示例1");
check(s.isAnagram("a", "ab"), false, "长度不等");
}
private static void check(boolean actual, boolean expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(1)(固定 26)。
解法二:时间 O(n log n),空间 O(log n)(排序栈)。进阶:空间 O(|字符集|)。
243. Shortest Word Distance(最短单词距离)
题目描述
给定一个字符串数组 wordsDict 和两个字符串 word1、word2,
返回这两个单词在列表中出现的最短距离。
- 输入 :
String[]wordsDict,Stringword1,Stringword2(2 <=wordsDict.length<= 3 * 10^4) - 输出 :
int(word1与word2最近出现位置的距离)
示例:words = ["practice", "makes", "perfect", "coding", "makes"]
word1="coding",word2="practice" -> 3
word1="makes",word2="coding" -> 1
约束: word1不等于word2;word1与word2均出现在数组wordsDict中。
解题思路
一遍扫描维护两个最近下标
- 解法一(双指针/最近下标):遍历数组,遇到 word1 更新 i1、遇到 word2 更新 i2;
两个下标都有效时更新答案 = min(ans, |i1 - i2|)。
为什么"最近出现位置"即可:任意两个出现位置之间必然隔着更接近的两个,
用最近下标两两比较不会漏最优解。 - 解法二(预存位置 + 双指针归并):先建 词->位置列表 的哈希表,再对两个列表
归并求最小差。适合多次查询场景(即 244 的预处理思想),单次查询也可用。
代码实现
java
public class Solution {
public int shortestDistance(String[] wordsDict, String word1, String word2) {
int i1 = -1, i2 = -1;
int ans = Integer.MAX_VALUE;
for (int i = 0; i < wordsDict.length; i++) {
if (wordsDict[i].equals(word1)) {
i1 = i;
} else if (wordsDict[i].equals(word2)) {
i2 = i;
}
if (i1 != -1 && i2 != -1) {
ans = Math.min(ans, Math.abs(i1 - i2));
}
}
return ans;
}
public int shortestDistancePreindex(String[] wordsDict, String word1, String word2) {
java.util.List<Integer> pos1 = new java.util.ArrayList<>();
java.util.List<Integer> pos2 = new java.util.ArrayList<>();
for (int i = 0; i < wordsDict.length; i++) {
if (wordsDict[i].equals(word1)) pos1.add(i);
else if (wordsDict[i].equals(word2)) pos2.add(i);
}
int ans = Integer.MAX_VALUE;
int i = 0, j = 0;
while (i < pos1.size() && j < pos2.size()) {
int diff = Math.abs(pos1.get(i) - pos2.get(j));
ans = Math.min(ans, diff);
if (pos1.get(i) < pos2.get(j)) i++;
else j++;
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
String[] words = {"practice", "makes", "perfect", "coding", "makes"};
check(s.shortestDistance(words, "coding", "practice"), 3, "示例1");
check(s.shortestDistance(words, "makes", "coding"), 1, "示例2");
check(s.shortestDistancePreindex(words, "coding", "practice"), 3, "预存位置法示例1");
check(s.shortestDistancePreindex(words, "makes", "coding"), 1, "预存位置法示例2");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
解法一:时间 O(n),空间 O(1)。解法二:时间 O(n)(建表+归并),空间 O(n)。
244. Shortest Word Distance II(最短单词距离 II)
题目描述
设计一个类 WordDistance,接收一个字符串数组 wordsDict 初始化,
并实现方法:给定两个单词 word1、word2(两者不同,且都出现在列表中),
返回它们之间的最短距离。该方法会被多次调用,因此需要尽可能高效。
- 输入 :构造方法
String[]wordsDict;查询Stringword1,Stringword2 - 输出 :
int(最短距离)
示例:wordsDict = ["practice", "makes", "perfect", "coding", "makes"]
newWordDistance(wordsDict);
wordDistance.shortest("coding", "practice") -> 3
wordDistance.shortest("makes", "coding") -> 1
约束: word1不等于word2;word1和word2均存在于列表中。
解题思路
预处理:词 -> 全部出现下标列表;查询:双指针归并求最小差
- 构造:遍历一次数组,用 Map<String, List> 记录每个词的所有下标(天然升序)。
- 查询:对 word1、word2 的两个下标列表做双指针归并;
每次取 |ai - bj| 更新答案,谁小谁前进(贪心逼近)。
时间 O(m + n),m、n 为两词出现次数。因为列表升序,双指针不会漏最优解。
为什么预处理能加速多次查询:每词下标列表长度远小于全数组,查询从 O(N)
降到 O(出现次数和)。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WordDistance {
private Map<String, List<Integer>> map;
public WordDistance(String[] wordsDict) {
map = new HashMap<>();
for (int i = 0; i < wordsDict.length; i++) {
map.computeIfAbsent(wordsDict[i], k -> new ArrayList<>()).add(i);
}
}
public int shortest(String word1, String word2) {
List<Integer> l1 = map.get(word1);
List<Integer> l2 = map.get(word2);
int i = 0, j = 0;
int ans = Integer.MAX_VALUE;
while (i < l1.size() && j < l2.size()) {
int a = l1.get(i), b = l2.get(j);
ans = Math.min(ans, Math.abs(a - b));
if (a < b) i++; else j++; // 让更小的下标前进,距离才可能缩小
}
return ans;
}
public static void main(String[] args) {
WordDistance wd = new WordDistance(new String[]{"practice", "makes", "perfect", "coding", "makes"});
check(wd.shortest("coding", "practice"), 3, "示例1");
check(wd.shortest("makes", "coding"), 1, "示例2");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
构造:时间 O(N),空间 O(N)。查询:时间 O(m + n),空间 O(1)。
245. Shortest Word Distance III(最短单词距离 III)
题目描述
给定一个字符串数组 wordsDict 和两个字符串 word1、word2,
返回这两个单词在列表中出现的最短距离。
本题与 243 的唯一区别:word1 和 word2 可能相同,它们代表列表中两个单独的单词
(即 word1 == word2 时,需要找该单词两次不同出现位置之间的最短距离)。
- 输入 :
String[]wordsDict,Stringword1,Stringword2 - 输出 :
int(最短距离)
示例:words = ["practice", "makes", "perfect", "coding", "makes"]
word1="makes",word2="coding" -> 1
word1="makes",word2="makes" -> 3
约束: word1和word2均出现在数组中;数组中至少有两次不同位置可满足word1==word2的查询。
解题思路
单遍扫描 + 延迟赋值处理同词
相同词时不能"各自维护一个下标再求差"(那样差恒为 0),需要记录上一次出现位置:
- 当 word1.equals(word2) 时:遍历过程遇到该词,用"上一次出现位置"算距离,
再更新 prev = i。这等价于相邻两次出现之间的距离(相邻是最短的)。 - 当 word1 != word2 时:用 243 的双下标法。
- 统一写法:维护 last 表示"上一次匹配到 word1 或 word2 的位置",同时用 idx1/idx2
记录当前各词最近位置;若两词相同则令 idx2 = idx1 再更新 idx1,即可复用同一循环。
为什么相邻出现即最短:同词多次出现之间按位置排序,任意两次之间的区间内必有
更近的相邻对,所以只需检查相邻出现。
代码实现
java
public class Solution {
public int shortestWordDistance(String[] wordsDict, String word1, String word2) {
long idx1 = -1, idx2 = -1; // 用 long 防止 -1 与 0 的减法边界误判
int ans = Integer.MAX_VALUE;
boolean same = word1.equals(word2);
for (int i = 0; i < wordsDict.length; i++) {
if (wordsDict[i].equals(word1)) {
if (same) {
idx2 = idx1; // 相同词:原偏移位给 word2,之后 word1 更新到当前
}
idx1 = i;
} else if (!same && wordsDict[i].equals(word2)) {
idx2 = i;
}
if (idx1 >= 0 && idx2 >= 0) {
ans = (int) Math.min(ans, Math.abs(idx1 - idx2));
}
}
return ans;
}
public int shortestWordDistance2(String[] wordsDict, String word1, String word2) {
int prev = -1; // 最近一次匹配位置(任意一个词)
int idx1 = -1, idx2 = -1;
int ans = Integer.MAX_VALUE;
for (int i = 0; i < wordsDict.length; i++) {
int matchIdx = -1;
if (wordsDict[i].equals(word1)) matchIdx = i;
else if (wordsDict[i].equals(word2)) matchIdx = i;
if (matchIdx == -1) continue;
if (word1.equals(word2)) {
// 同词:用上一次出现位置计算
if (prev != -1) ans = Math.min(ans, i - prev);
prev = i;
} else {
// 异词:更新对应下标
if (wordsDict[i].equals(word1)) idx1 = i; else idx2 = i;
if (idx1 != -1 && idx2 != -1) ans = Math.min(ans, Math.abs(idx1 - idx2));
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
String[] words = {"practice", "makes", "perfect", "coding", "makes"};
check(s.shortestWordDistance(words, "makes", "coding"), 1, "示例1-异词");
check(s.shortestWordDistance(words, "makes", "makes"), 3, "示例2-同词");
check(s.shortestWordDistance2(words, "makes", "coding"), 1, "解法二示例1");
check(s.shortestWordDistance2(words, "makes", "makes"), 3, "解法二示例2");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
时间 O(n),空间 O(1)。
246. Strobogrammatic Number(中心对称数)
题目描述
中心对称数是指一个数字旋转 180 度之后看起来依旧相同的数字(或者说上下颠倒看)。
请写一个函数来判断一个数字是否为中心对称数。数字以字符串形式给出。
- 输入 :
Stringnum(数字字符串) - 输出 :
boolean(是否中心对称)
示例 1:
num="69" -> true
示例 2:
num="88" -> true
示例 3:
num="962" -> false
约束:
num由数字字符组成(确保输入合法)。
解题思路
旋转映射双指针
旋转 180° 后合法的数字映射:0->0,1->1,6->9,8->8,9->6;
其余数字(2/3/4/5/7)旋转后无意义,直接不合法。
- 解法一(双指针 + 映射表):左指针依次取数,检查它的旋转值是否等于右指针的数;
每步向内移动。中间单个字符必须是 0/1/8(自旋转映射)。 - 解法二(构造反转串):对 num 逐字符替换为旋转值得到 rotated,
再整体反转,比较是否等于原串;等价于"从右到左逐位检查映射"。
为什么双指针正确:中心对称要求"位置 i 的字符旋转后恰好等于位置 n-1-i 的字符",
这正是镜像对称,逐对检查即可。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
private static final Map<Character, Character> ROT = new HashMap<>();
static {
ROT.put('0', '0');
ROT.put('1', '1');
ROT.put('6', '9');
ROT.put('8', '8');
ROT.put('9', '6');
}
public boolean isStrobogrammatic(String num) {
int i = 0, j = num.length() - 1;
while (i <= j) {
char a = num.charAt(i), b = num.charAt(j);
// a 必须可旋转,且旋转后等于 b
if (!ROT.containsKey(a) || ROT.get(a) != b) {
return false;
}
i++;
j--;
}
return true;
}
public boolean isStrobogrammaticRotate(String num) {
StringBuilder sb = new StringBuilder();
for (char c : num.toCharArray()) {
Character r = ROT.get(c);
if (r == null) return false; // 不存在旋转映射
sb.append(r);
}
return sb.reverse().toString().equals(num);
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.isStrobogrammatic("69"), true, "示例1");
check(s.isStrobogrammatic("88"), true, "示例2");
check(s.isStrobogrammatic("962"), false, "示例3");
check(s.isStrobogrammatic("818"), true, "补充:818");
check(s.isStrobogrammatic("1"), true, "单字符1");
check(s.isStrobogrammaticRotate("69"), true, "旋转串法示例1");
check(s.isStrobogrammaticRotate("962"), false, "旋转串法示例3");
}
private static void check(boolean actual, boolean expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
时间 O(n),空间 O(1)(映射表固定大小)。
247. Strobogrammatic Number II(中心对称数 II)
题目描述
给定一个整数 n,返回所有长度为 n 的中心对称数。
中心对称数是指一个数字旋转 180 度之后看起来依旧相同的数字。
- 输入 :
intn(1 <=n<= 14) - 输出 :
List<String>(所有长度为n的中心对称数)
示例 1:
n=2 -> ["11","69","88","96"]
示例 2:
n=1 -> ["0","1","8"]
约束:
- 1 <=
n<= 14。
解题思路
从中间向外构造(递归/迭代,长度增减 2)
旋转对称的对子:0-0、1-1、6-9、8-8、9-6。长度为 n 的数由长度为 n-2 的数
外面套一对旋转对子得到。可以参考 n=0("")与 n=1("0","1","8")为基底:
- 解法一(递归):construct(m) 生成全部长度为 m 的中心对称数;
if (m == 0) 返回 "";if (m == 1) 返回 "0","1","8";
否则对 construct(m-2) 的每个串,在外面套上 5 种对子。
调用入口:construct(n),但最外层不能以 '0' 开头,需要额外过滤。 - 解法二(从左到右 DFS 填空):用字符数组从两端向中间填,两侧成对放置,
中间位(奇数长度时)只能放 0/1/8;首位不能为 0。
为什么长度每次减 2:最外一对字符占据两端,去掉后内部剩余位数为 m-2,
因此以 n 或 n-1 之差递归(题目提示 n-2 而非 n-1)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
public List<String> findStrobogrammatic(int n) {
List<String> res = build(n);
// 最外层不能以 0 开头(除非 n == 1)
if (n > 1) res.removeIf(s -> s.startsWith("0"));
return res;
}
private List<String> build(int m) {
if (m == 0) return new ArrayList<>(Arrays.asList(""));
if (m == 1) return new ArrayList<>(Arrays.asList("0", "1", "8"));
List<String> inner = build(m - 2); // 内部所有中心对称数
List<String> res = new ArrayList<>();
String[][] pairs = {{"0","0"},{"1","1"},{"6","9"},{"8","8"},{"9","6"}};
for (String s : inner) {
for (String[] p : pairs) {
res.add(p[0] + s + p[1]);
}
}
return res;
}
public List<String> findStrobogrammaticDFS(int n) {
List<String> res = new ArrayList<>();
char[] buf = new char[n];
dfs(buf, 0, n - 1, res);
return res;
}
private static final char[][] PAIRS = {{'0','0'},{'1','1'},{'6','9'},{'8','8'},{'9','6'}};
private void dfs(char[] buf, int l, int r, List<String> res) {
if (l > r) {
res.add(new String(buf));
return;
}
if (l == r) {
// 中间位只能放自旋转数字
for (char c : new char[]{'0','1','8'}) {
buf[l] = c;
dfs(buf, l + 1, r - 1, res);
}
return;
}
for (char[] p : PAIRS) {
// 最外层不能以 0 开头
if (l == 0 && p[0] == '0') continue;
buf[l] = p[0];
buf[r] = p[1];
dfs(buf, l + 1, r - 1, res);
}
}
public static void main(String[] args) {
Solution s = new Solution();
check(equalList(s.findStrobogrammatic(2), new ArrayList<>(Arrays.asList("11","69","88","96"))), true, "示例1");
check(equalList(s.findStrobogrammatic(1), new ArrayList<>(Arrays.asList("0","1","8"))), true, "示例2");
check(equalList(s.findStrobogrammatic(3), new ArrayList<>(Arrays.asList("101","111","181","609","619","689","808","818","888","906","916","986"))), true, "n=3");
check(equalList(s.findStrobogrammaticDFS(2), new ArrayList<>(Arrays.asList("11","69","88","96"))), true, "DFS法示例1");
}
private static void check(boolean actual, boolean expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
解法一:时间 O(5^(n/2)) 级别(结果数量级),空间 O(结果数 * n)。
248. Strobogrammatic Number III(中心对称数 III)
题目描述
给定两个字符串 low 和 high,表示两个整数 low, high,
请统计这个区间内中心对称数的个数。中心对称数是旋转 180 度后仍相同的数字。
- 输入 :
Stringlow,Stringhigh(low 和 high 由数字组成,low <= high) - 输出 :
int(区间内中心对称数的个数)
示例 1:
low="50", high="100" -> 3(69、88、96)
示例 2:
low="0", high="0" -> 1(0)
约束:
- 由两个字符串给出的数字保证 low <= high。
解题思路
枚举所有长度在 len(low), len(high) 的中心对称数并过滤
中心对称数至多 5^7 个(长度 <= 14 时,每加 2 位乘 5,即 5 种对子),
因此可以先生成再按数值过滤:
- 生成:复用 247 的"从中间向外构造",对每个长度 len 生成全部中心对称数;
- 过滤:去除前导 0(长度 > 1 时);字符串按字典序比较即数值比较(等长时),
跨长度时用长度判断即可覆盖区间。 - 等长比较技巧:把 low 与 high 补齐到与目标长度相同再按字典序比较,或用 BigDecimal。
- 进阶:可以按"最低长度基准"优化:长度恰好等于 len(low) 时要求 >= low,
恰好等于 len(high) 时要求 <= high,中间长度全部计入。
为什么枚举可行:n <= 14 因此最长 14 位,总生成量 = (5^1 + 5^2 + ... + 5^7 级别),
在 10^4 量级以内,性能无忧。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
private static final char[][] PAIRS = {{'0','0'},{'1','1'},{'6','9'},{'8','8'},{'9','6'}};
private int count = 0;
public int strobogrammaticInRange(String low, String high) {
count = 0;
for (int len = low.length(); len <= high.length(); len++) {
char[] buf = new char[len];
dfs(buf, 0, len - 1, low, high);
}
return count;
}
private void dfs(char[] buf, int l, int r, String low, String high) {
if (l > r) {
String num = new String(buf);
if (!isNoLeadingZero(num)) return; // 长度 > 1 不允许前导 0
if (inRange(num, low, high)) count++;
return;
}
if (l == r) {
for (char c : new char[]{'0','1','8'}) { // 中间位
buf[l] = c;
dfs(buf, l + 1, r - 1, low, high);
}
return;
}
for (char[] p : PAIRS) {
buf[l] = p[0];
buf[r] = p[1];
dfs(buf, l + 1, r - 1, low, high);
}
}
private boolean isNoLeadingZero(String s) {
return s.length() == 1 || s.charAt(0) != '0';
}
private boolean inRange(String num, String low, String high) {
if (num.length() < low.length() || num.length() > high.length()) return false;
// 与 low 比较
if (num.length() > low.length()) {
// 长度更长必然 > low
} else if (num.compareTo(low) < 0) {
return false;
}
// 与 high 比较
if (num.length() < high.length()) {
// 长度更短必然 < high
} else if (num.compareTo(high) > 0) {
return false;
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
check(s.strobogrammaticInRange("50", "100"), 3, "示例1");
check(s.strobogrammaticInRange("0", "0"), 1, "示例2");
check(s.strobogrammaticInRange("0", "9"), 3, "补充:0,1,8");
check(s.strobogrammaticInRange("1", "100"), 6, "补充:1,8,11,69,88,96");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
时间 O(5^(N/2))(N=high 长度),空间 O(结果数)。
249. Group Shifted Strings(移位字符串分组)
题目描述
给定一个字符串数组 strings,将其中的字符串按照"移位序列"分组。
移位序列是指:将字符串 s 的每个字母沿字母表向前/向后移动相同位数得到的字符串。
例如 "abc" -> "bcd" -> ... -> "xyz"。
注意:"az" 与 "ba" 属于同一移位序列(z 往后移到 a,即循环移位)。
- 输入 :
String[]strings(1 <= strings.length <= 200,1 <= stringsi.length <= 50) - 输出 :
List<List<String>>(每个分组内的字符串按字典序排列)
示例:strings = ["abc","bcd","acef","xyz","az","ba","a","z"]
-> \["abc","bcd","xyz","az","ba","acef","`a`","z"]
约束: - strings 由小写字母组成;1 <= strings.length <= 200;1 <= stringsi.length <= 50。
解题思路
标准化"差分键"哈希分组
两个串能互相移位得到,当且仅当它们的"相邻字符差的循环序列"完全相同:
- 对每个字符串,相邻字符差 d = (si - si-1 + 26) % 26;
把差分序列拼接成标准化键(如 "abc" -> "11","bcd" -> "11","xyz" -> "11";"az" -> "25",
"ba" -> "25")。 - 同一键的字符串必然同一组。
为什么 % 26:字母表循环,'z' 移到 'a' 的差为 25(取模后仍用正整数表达)。 - 分组后每组内部按字典序排序(题目要求)。
为什么差分键充分必要:若 A 能移位得到 B,则对应字符差完全一致;反之亦然。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
public List<List<String>> groupStrings(String[] strings) {
Map<String, List<String>> map = new HashMap<>();
for (String s : strings) {
String key = encode(s);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
List<List<String>> res = new ArrayList<>();
for (List<String> group : map.values()) {
java.util.Collections.sort(group); // 组内按字典序
res.add(group);
}
return res;
}
private String encode(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i < s.length(); i++) {
int diff = (s.charAt(i) - s.charAt(i - 1) + 26) % 26; // 处理循环差
sb.append(diff).append(',');
}
return sb.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
List<List<String>> r = s.groupStrings(new String[]{"abc","bcd","acef","xyz","az","ba","a","z"});
// 校验分组数 = 4
check(r.size(), 4, "示例:分组数");
boolean hasAbcGroup = false, hasAzGroup = false;
for (List<String> g : r) {
if (g.toString().equals("[abc, bcd, xyz]")) hasAbcGroup = true;
if (g.toString().equals("[az, ba]")) hasAzGroup = true;
}
check(hasAbcGroup, true, "示例:abc组");
check(hasAzGroup, true, "示例:az组");
check(s.groupStrings(new String[]{"a","a"}).size(), 1, "重复串");
check(s.groupStrings(new String[]{"ab"}).size(), 1, "单串");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
private static void check(boolean actual, boolean expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
时间 O(∑len * avgLen)(标准化每个串 O(len)),空间 O(∑len)。
250. Count Univalue Subtrees(统计同值子树)
题目描述
给定一个二叉树的根节点 root,统计该二叉树中"同值子树"的数量。
同值子树是指子树中所有节点都有相同值的子树。
- 输入 :
TreeNoderoot(树的节点数范围 0, 1000) - 输出 :
int(同值子树个数)
示例:二叉树下(值):
5
/
1 5
/ \
5 5 5
返回 4(三个叶节点 5 各自是同值子树,加上根节点的右子树 5-\>5 也是同值子树)
约束: - 树的节点数范围 0, 1000;节点值范围为 -1000, 1000。
解题思路
后序遍历的"同值性"自底向上判定
一个节点为根的子树是同值子树,当且仅当:
- 左右子树都(为空或)是同值子树;
- 且左右子节点(若存在)的值都等于当前节点值。
- 解法一(递归返回 boolean):后序遍历每个节点,子树的同值性作为返回值向上汇总,
每次返回 true 时计数 +1。 - 解法二(全局计数 + 递归判定):与解法一本质相同,只是把计数作为类变量累计。
本代码实现同时给出"不带类变量"的变体(计数用数组/引用包装),供对比。
为什么后序:判断父节点必须已知子树内部是否同值,只能自底向上。
代码实现
java
public class Solution {
private int count = 0;
public int countUnivalSubtrees(TreeNode root) {
count = 0;
isUnival(root);
return count;
}
private boolean isUnival(TreeNode node) {
if (node == null) return true; // 空树视为同值
boolean leftOk = isUnival(node.left);
boolean rightOk = isUnival(node.right);
// 若左子树不为空且值与当前节点不同,则非同值
if (leftOk && rightOk
&& (node.left == null || node.left.val == node.val)
&& (node.right == null || node.right.val == node.val)) {
count++;
return true;
}
return false;
}
public int countUnivalSubtreesNoField(TreeNode root) {
int[] cnt = new int[1]; // 用数组包装,避免类变量
isUnivalNoField(root, cnt);
return cnt[0];
}
private boolean isUnivalNoField(TreeNode node, int[] cnt) {
if (node == null) return true;
boolean leftOk = isUnivalNoField(node.left, cnt);
boolean rightOk = isUnivalNoField(node.right, cnt);
if (leftOk && rightOk
&& (node.left == null || node.left.val == node.val)
&& (node.right == null || node.right.val == node.val)) {
cnt[0]++;
return true;
}
return false;
}
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) {
Solution s = new Solution();
// 5
// / \
// 1 5
// / \ \
// 5 5 5
TreeNode r1 = build(new Integer[]{5,1,5,5,5,null,5});
check(s.countUnivalSubtrees(r1), 4, "示例1");
check(s.countUnivalSubtreesNoField(r1), 4, "无类变量版示例1");
check(s.countUnivalSubtrees(build(new Integer[]{})), 0, "空树");
check(s.countUnivalSubtrees(build(new Integer[]{5,5,5,5,5,null,5})), 6, "全同值");
check(s.countUnivalSubtrees(build(new Integer[]{1,2,3})), 2, "根不同值:两叶子");
}
private static void check(int actual, int expected, String name) {
System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
+ " | actual=" + actual + " expected=" + expected);
if (expected != actual) System.out.println(" <<< 断言失败: " + name);
}
}
复杂度分析
两种解法时间均 O(n),空间 O(h)(递归栈深)。