LeetCode 76-100 题解合集
文章目录
- [LeetCode 76-100 题解合集](#LeetCode 76-100 题解合集)
-
- [76. Minimum Window Substring(最小覆盖子串)](#76. Minimum Window Substring(最小覆盖子串))
- [77. Combinations(组合)](#77. Combinations(组合))
- [78. Subsets(子集)](#78. Subsets(子集))
- [79. Word Search(单词搜索)](#79. Word Search(单词搜索))
- [80. Remove Duplicates from Sorted Array II(删除有序数组中的重复项 II)](#80. Remove Duplicates from Sorted Array II(删除有序数组中的重复项 II))
- [81. Search in Rotated Sorted Array II(搜索旋转排序数组 II)](#81. Search in Rotated Sorted Array II(搜索旋转排序数组 II))
- [82. Remove Duplicates from Sorted List II(删除排序链表中的重复元素 II)](#82. Remove Duplicates from Sorted List II(删除排序链表中的重复元素 II))
- [83. Remove Duplicates from Sorted List(删除排序链表中的重复元素)](#83. Remove Duplicates from Sorted List(删除排序链表中的重复元素))
- [84. Largest Rectangle in Histogram(柱状图中最大的矩形)](#84. Largest Rectangle in Histogram(柱状图中最大的矩形))
- [85. Maximal Rectangle(最大矩形)](#85. Maximal Rectangle(最大矩形))
- [86. Partition List(分隔链表)](#86. Partition List(分隔链表))
- [87. Scramble String(扰乱字符串)](#87. Scramble String(扰乱字符串))
- [88. Merge Sorted Array(合并两个有序数组)](#88. Merge Sorted Array(合并两个有序数组))
- [89. Gray Code(格雷编码)](#89. Gray Code(格雷编码))
- [90. Subsets II(子集 II)](#90. Subsets II(子集 II))
- [91. Decode Ways(解码方法)](#91. Decode Ways(解码方法))
- [92. Reverse Linked List II(反转链表 II)](#92. Reverse Linked List II(反转链表 II))
- [93. Restore IP Addresses(复原 IP 地址)](#93. Restore IP Addresses(复原 IP 地址))
- [94. Binary Tree Inorder Traversal(二叉树的中序遍历)](#94. Binary Tree Inorder Traversal(二叉树的中序遍历))
- [95. Unique Binary Search Trees II(不同的二叉搜索树 II)](#95. Unique Binary Search Trees II(不同的二叉搜索树 II))
- [96. Unique Binary Search Trees(不同的二叉搜索树)](#96. Unique Binary Search Trees(不同的二叉搜索树))
- [97. Interleaving String(交错字符串)](#97. Interleaving String(交错字符串))
- [98. Validate Binary Search Tree(验证二叉搜索树)](#98. Validate Binary Search Tree(验证二叉搜索树))
- [99. Recover Binary Search Tree(恢复二叉搜索树)](#99. Recover Binary Search Tree(恢复二叉搜索树))
- [100. Same Tree(相同的树)](#100. Same Tree(相同的树))
76. Minimum Window Substring(最小覆盖子串)
题目描述
给你一个字符串 s、一个字符串 t。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""。如果存在多个最短子串,返回任意一个。
注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
示例:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
约束: 1 <= s.length, t.length <= 10^5,s 和 t 由英文字母组成
解题思路
核心思想:滑动窗口 + 欠账计数------窗口收缩时找出所有覆盖串中的最短。
解法:滑动窗口(推荐) 。先统计 t 的字符需求 need 与欠账数 needCount。右指针扩展窗口补欠账,当 needCount == 0(完全覆盖)时尝试收缩左指针:若左移后仍覆盖则更新最短区间并继续左移。时间 O(n)(左右指针各移动 n 次),空间 O(字符集)。
代码实现
java
class Solution {
// 滑动窗口(推荐)
public String minWindow(String s, String t) {
int[] need = new int[128];
for (char c : t.toCharArray()) need[c]++;
int needCount = t.length();
int left = 0, minLen = Integer.MAX_VALUE, minStart = 0;
for (int right = 0; right < s.length(); right++) {
char rc = s.charAt(right);
if (need[rc] > 0) needCount--; // 是需要的字符,欠账减少
need[rc]--;
while (needCount == 0) { // 覆盖 t 全部字符
if (right - left + 1 < minLen) { // 更新最短
minLen = right - left + 1;
minStart = left;
}
char lc = s.charAt(left);
need[lc]++;
if (need[lc] > 0) needCount++; // 移出后欠账恢复
left++;
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(minStart, minStart + minLen);
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.minWindow("ADOBECODEBANC", "ABC")); // BANC
System.out.println(s.minWindow("a", "a")); // a
System.out.println(s.minWindow("a", "aa")); // (空)
}
}
复杂度分析
- 时间复杂度:O(n) --- 双指针各扫一遍
- 空间复杂度:O(1) --- 固定 128 计数数组
77. Combinations(组合)
题目描述
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按任何顺序返回答案。
示例:
输入:n = 4, k = 2
输出:[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4]]
约束: 1 <= n <= 20,1 <= k <= n
解题思路
核心思想:回溯------从 start 开始选数,保证组合内升序不重复。
解法:回溯(推荐) 。dfs(start, k, path):path 满 k 个记录;从 start...n 选数加入并递归 start+1。剪枝:还需元素数 > 剩余可选数时可提前终止。时间 O(C(n,k)×k),空间 O(k)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
private List<List<Integer>> res = new ArrayList<List<Integer>>();
// 回溯(推荐)
public List<List<Integer>> combine(int n, int k) {
dfs(n, k, 1, new ArrayList<Integer>());
return res;
}
private void dfs(int n, int k, int start, List<Integer> path) {
if (path.size() == k) {
res.add(new ArrayList<Integer>(path));
return;
}
for (int i = start; i <= n; i++) {
path.add(i);
dfs(n, k, i + 1, path); // 不回头,保证不重复
path.remove(path.size() - 1);
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.combine(4, 2)); // [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
}
}
复杂度分析
- 时间复杂度:O(C(n,k)×k) --- 组合数 × 复制成本
- 空间复杂度:O(k) --- 递归栈
78. Subsets(子集)
题目描述
给你一个整数数组 nums,数组中的元素互不相同 。返回该数组所有可能的子集(幂集),解集不能 包含重复的子集。你可以按任意顺序返回解集。
示例:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
约束: 1 <= nums.length <= 10,-10 <= nums[i] <= 10,nums 中所有元素互不相同
解题思路
核心思想:每个元素"选或不选",可回溯或按位枚举。
解法一:回溯(推荐) 。对每个位置可选可不选:加入 path 递归选,再撤销递归不选;也可按 start 遍历式选入路径(每进入一层就记录当前 path 为子集)。时间 O(2^n),空间 O(n)。
解法二:位运算。2^n 个子集,每个 mask 的二进制位表示是否取 numsi。时间 O(n×2^n),空间 O(1)(不含结果)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
private List<List<Integer>> res = new ArrayList<List<Integer>>();
// 解法一:回溯(推荐)
public List<List<Integer>> subsets(int[] nums) {
dfs(nums, 0, new ArrayList<Integer>());
return res;
}
private void dfs(int[] nums, int idx, List<Integer> path) {
res.add(new ArrayList<Integer>(path)); // 当前路径即一个子集
for (int i = idx; i < nums.length; i++) {
path.add(nums[i]);
dfs(nums, i + 1, path);
path.remove(path.size() - 1);
}
}
// 解法二:位运算
public List<List<Integer>> subsetsBit(int[] nums) {
int n = nums.length;
List<List<Integer>> res = new ArrayList<List<Integer>>();
for (int mask = 0; mask < (1 << n); mask++) {
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) list.add(nums[i]); // 第 i 位为 1 则取
}
res.add(list);
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.subsets(new int[]{1,2,3})); // 8 个子集
System.out.println(s.subsetsBit(new int[]{1,2,3}).size()); // 8
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 回溯(推荐) | O(2^n) | O(n) | 累加路径 |
| 位运算 | O(n×2^n) | O(1)(不含结果) | 枚举 mask |
79. Word Search(单词搜索)
题目描述
给定一个 m x n 二维字符网格 board 和一个字符串单词 word。如果 word 存在于网格中,返回 true;否则返回 false。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
输入:同一网格, word = "ABCB"
输出:false
约束: 1 <= m, n <= 6,1 <= word.length <= 15,board 和 word 仅由大小写英文字母组成
解题思路
核心思想:DFS 回溯------从每个格子作为起点搜单词,用"访问标记"防重复使用。
解法:DFS + 回溯(推荐) 。对每个格子,若首字母匹配则进入 dfs:向上下左右四个方向扩展,匹配单词下一字符;不匹配或越界返回 false;匹配完整个单词返回 true。用临时把当前格子字符改为 # 作为访问标记(原地),回溯时还原。时间 O(m×n×4^L)(L=单词长度),空间 O(L)(递归栈)。
代码实现
java
class Solution {
private int m, n;
private int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
// DFS + 回溯(推荐)
public boolean exist(char[][] board, String word) {
m = board.length; n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (dfs(board, word, i, j, 0)) return true; // 任意起点成功即可
}
}
return false;
}
private boolean dfs(char[][] board, String word, int i, int j, int idx) {
if (idx == word.length()) return true; // 全部匹配
if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != word.charAt(idx)) return false;
char tmp = board[i][j];
board[i][j] = '#'; // 访问标记
for (int[] d : dirs) {
if (dfs(board, word, i + d[0], j + d[1], idx + 1)) return true;
}
board[i][j] = tmp; // 回溯还原
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
char[][] b = {{'A','B','C','E'},{'S','F','C','S'},{'A','D','E','E'}};
System.out.println(s.exist(b, "ABCCED")); // true
System.out.println(s.exist(b, "ABCB")); // false
}
}
复杂度分析
- 时间复杂度:O(m×n×4^L) --- 每次最多四方向分支
- 空间复杂度:O(L) --- 递归深度
80. Remove Duplicates from Sorted Array II(删除有序数组中的重复项 II)
题目描述
给你一个有序数组 nums,请你原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次,返回删除后数组的新长度。不要使用额外的数组空间,必须保持相对顺序。
示例:
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]
约束: 1 <= nums.length <= 3 * 10^4,-10^4 <= nums[i] <= 10^4
解题思路
核心思想:快慢指针------每个元素最多保留 2 个,慢指针指向下一个可写位置。
解法:快慢指针(推荐) 。i 慢指针(已保留区终点),j 快指针。nums[j] 与 nums[i-2] 比较:不同则可以保留(因为 i-2 之前是"第 i-2 个的计数边界",相同则说明 numsj 同类已出现过 2 次)。时间 O(n),空间 O(1)。
代码实现
java
import java.util.Arrays;
class Solution {
// 快慢指针(推荐)
public int removeDuplicates(int[] nums) {
int i = 0;
for (int j = 0; j < nums.length; j++) {
if (i < 2 || nums[j] != nums[i - 2]) { // 与"上上个保留值"不同才保留
nums[i++] = nums[j];
}
}
return i;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] a = {1,1,1,2,2,3};
int len = s.removeDuplicates(a);
System.out.println(len + ", " + Arrays.toString(Arrays.copyOf(a, len))); // 5, [1,1,2,2,3]
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
81. Search in Rotated Sorted Array II(搜索旋转排序数组 II)
题目描述
已知存在一个按非降序排列的整数数组 nums,数组中的值不必互不相同 。在传递给函数之前,nums 在预先未知的某个下标上进行了旋转,使数组变为有序数组旋转若干次的形态。给你旋转后的数组 nums 和一个整数 target,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target,则返回 true,否则返回 false。你必须尽可能减少整个操作步骤。
示例:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false
约束: 1 <= nums.length <= 5000,-10^4 <= nums[i], target <= 10^4
解题思路
核心思想:与 33 题类似,但有重复元素后边界条件失效,需要先"去重"(左右端重复时收缩)再二分。
解法:二分 + 重复处理(推荐) 。当 nums[left] == nums[mid] 时无法判断哪边有序,直接 left++ 收缩一步。其余情况沿用 33 题的"有序半区判断"。最坏情况全是相同元素退化为 O(n),平均 O(log n)。时间 O(log n)~O(n),空间 O(1)。
代码实现
java
class Solution {
// 二分 + 重复处理(推荐)
public boolean search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return true;
if (nums[left] == nums[mid] && nums[mid] == nums[right]) {
left++; right--; // 三个相等无法判断,两端收缩
} else if (nums[left] <= nums[mid]) { // 左半有序
if (nums[left] <= target && target < nums[mid]) right = mid - 1;
else left = mid + 1;
} else { // 右半有序
if (nums[mid] < target && target <= nums[right]) left = mid + 1;
else right = mid - 1;
}
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.search(new int[]{2,5,6,0,0,1,2}, 0)); // true
System.out.println(s.search(new int[]{2,5,6,0,0,1,2}, 3)); // false
System.out.println(s.search(new int[]{1,1,1,1,1}, 0)); // false
}
}
复杂度分析
- 时间复杂度:O(log n) 平均,O(n) 最坏(大量重复)
- 空间复杂度:O(1)
82. Remove Duplicates from Sorted List II(删除排序链表中的重复元素 II)
题目描述
给定一个已排序的链表的头 head,删除原始链表中所有重复数字的节点,只留下不同的数字。返回已排序的链表。
示例:
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
输入:head = [1,1,1,2,3]
输出:[2,3]
约束: 链表中节点数目在范围 [0, 300] 内,-100 <= Node.val <= 100,链表已按升序排列
解题思路
核心思想:用前驱指针扫描,发现重复段整段跳过。
解法:哑节点 + 前驱扫描(推荐) 。dummy 哨兵,prev 指向前驱。扫描中若 cur.next 与 cur 值相同,则继续后移直到跳过整段重复值,再 prev.next = 下一个不同值;否则 prev = prev.next。时间 O(n),空间 O(1)。
代码实现
java
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
// 哑节点 + 前驱扫描(推荐)
public ListNode deleteDuplicates(ListNode head) {
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy, cur = head;
while (cur != null) {
// 发现重复段
if (cur.next != null && cur.next.val == cur.val) {
int v = cur.val;
while (cur != null && cur.val == v) cur = cur.next; // 跳过整段
prev.next = cur; // 前驱直接接到段后
} else {
prev = cur; // 无重复,正常前进
cur = cur.next;
}
}
return dummy.next;
}
private static ListNode build(int[] arr) {
ListNode d = new ListNode(0), c = d;
for (int v : arr) { c.next = new ListNode(v); c = c.next; }
return d.next;
}
private static String str(ListNode h) {
StringBuilder sb = new StringBuilder("[");
while (h != null) { sb.append(h.val).append(h.next == null ? "" : ","); h = h.next; }
return sb.append("]").toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(str(s.deleteDuplicates(build(new int[]{1,2,3,3,4,4,5})))); // [1,2,5]
System.out.println(str(s.deleteDuplicates(build(new int[]{1,1,1,2,3})))); // [2,3]
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
83. Remove Duplicates from Sorted List(删除排序链表中的重复元素)
题目描述
给定一个已排序的链表的头 head,删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。
示例:
输入:head = [1,1,2]
输出:[1,2]
输入:head = [1,1,2,3,3]
输出:[1,2,3]
约束: 链表中节点数目在范围 [0, 300] 内,-100 <= Node.val <= 100,链表已按升序排列
解题思路
核心思想:单指针遍历,值相同则跳过,否则前进。
解法:单指针(推荐) 。cur = head,若 cur.next.val == cur.val 则 cur.next = cur.next.next(删除重复),否则 cur = cur.next。时间 O(n),空间 O(1)。
代码实现
java
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
// 单指针(推荐)
public ListNode deleteDuplicates(ListNode head) {
ListNode cur = head;
while (cur != null && cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next; // 删掉重复节点
} else {
cur = cur.next;
}
}
return head;
}
private static ListNode build(int[] arr) {
ListNode d = new ListNode(0), c = d;
for (int v : arr) { c.next = new ListNode(v); c = c.next; }
return d.next;
}
private static String str(ListNode h) {
StringBuilder sb = new StringBuilder("[");
while (h != null) { sb.append(h.val).append(h.next == null ? "" : ","); h = h.next; }
return sb.append("]").toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(str(s.deleteDuplicates(build(new int[]{1,1,2})))); // [1,2]
System.out.println(str(s.deleteDuplicates(build(new int[]{1,1,2,3,3})))); // [1,2,3]
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
84. Largest Rectangle in Histogram(柱状图中最大的矩形)
题目描述
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例:
输入:heights = [2,1,5,6,2,3]
输出:10
约束: 1 <= heights.length <= 10^5,0 <= heights[i] <= 10^4
解题思路
核心思想:单调递增栈------对每个柱子找到"左边第一个比它矮"和"右边第一个比它矮",两者之间就是它能撑起的最大宽度。
解法:单调栈(推荐) 。栈内按下标递增、高度递增。遍历每个高度:heights[i] 比栈顶矮时,弹出栈顶作为"高",其左右边界分别是栈内下一个元素与 i,面积 = 高 × (i - 左界 - 1)。末尾补一个高度 0 的哨兵,保证所有柱子出栈。时间 O(n)(每个下标进出栈一次),空间 O(n)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 单调栈(推荐)
public int largestRectangleArea(int[] heights) {
Deque<Integer> stack = new ArrayDeque<Integer>();
int ans = 0;
int n = heights.length;
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i]; // 末尾哨兵 0,强制出栈
while (!stack.isEmpty() && heights[stack.peek()] > h) {
int height = heights[stack.pop()]; // 以弹出柱高为矩形高
int width = stack.isEmpty() ? i : i - stack.peek() - 1; // 左右矮柱之间
ans = Math.max(ans, height * width);
}
stack.push(i);
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.largestRectangleArea(new int[]{2,1,5,6,2,3})); // 10
System.out.println(s.largestRectangleArea(new int[]{2,4})); // 4
}
}
复杂度分析
- 时间复杂度:O(n) --- 每个下标进出栈各一次
- 空间复杂度:O(n) --- 栈
85. Maximal Rectangle(最大矩形)
题目描述
给定一个仅包含 0 和 1、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
示例:
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:6
约束: 1 <= rows, cols <= 200,matrixij 为 '0' 或 '1'
解题思路
核心思想:把每一行当作"以该行为底的柱状图",逐行调用 84 题的单调栈算法。
解法:逐行柱状图 + 单调栈(推荐) 。维护 heights[col](当前行到顶部的连续 1 的高度):遇到 '1' 则 heights[j]++,遇 '0' 则归零。对每行用 84 题的单调栈求最大矩形,全局取最大值。时间 O(rows×cols),空间 O(cols)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 逐行柱状图 + 单调栈(推荐)
public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) return 0;
int cols = matrix[0].length, ans = 0;
int[] heights = new int[cols];
for (char[] row : matrix) {
for (int j = 0; j < cols; j++) {
heights[j] = row[j] == '1' ? heights[j] + 1 : 0; // 累计高度
}
ans = Math.max(ans, largestArea(heights)); // 复用 84 题算法
}
return ans;
}
private int largestArea(int[] heights) {
Deque<Integer> stack = new ArrayDeque<Integer>();
int ans = 0, n = heights.length;
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i];
while (!stack.isEmpty() && heights[stack.peek()] > h) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
ans = Math.max(ans, height * width);
}
stack.push(i);
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
char[][] m = {{'1','0','1','0','0'},{'1','0','1','1','1'},{'1','1','1','1','1'},{'1','0','0','1','0'}};
System.out.println(s.maximalRectangle(m)); // 6
System.out.println(s.maximalRectangle(new char[][]{{'0'}})); // 0
}
}
复杂度分析
- 时间复杂度:O(rows×cols) --- 每行一次单调栈
- 空间复杂度:O(cols) --- 高度数组与栈
86. Partition List(分隔链表)
题目描述
给你一个链表的头节点 head 和一个特定值 x,请你对链表进行分隔,使得所有小于 x 的节点都出现在大于或等于 x 的节点之前。你应当保留两个分区中每个节点的初始相对位置。
示例:
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]
约束: 链表中节点数目在范围 [0, 200] 内,-100 <= Node.val <= 100,-200 <= x <= 200
解题思路
核心思想:拆成"小链表"和"大链表"两条,最后拼接。
解法:双链表拼接(推荐) 。small 与 big 两个哨兵链表。遍历原链表,节点值 < x 挂到 small 尾,否则挂到 big 尾。最后 smallTail.next = big.next,bigTail.next = null。时间 O(n),空间 O(1)。
代码实现
java
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
// 双链表拼接(推荐)
public ListNode partition(ListNode head, int x) {
ListNode small = new ListNode(0), big = new ListNode(0);
ListNode sTail = small, bTail = big;
for (ListNode cur = head; cur != null; cur = cur.next) {
if (cur.val < x) { sTail.next = cur; sTail = cur; } // 小链表
else { bTail.next = cur; bTail = cur; } // 大链表
}
bTail.next = null; // 尾部断开,防成环
sTail.next = big.next; // 拼接
return small.next;
}
private static String str(ListNode h) {
StringBuilder sb = new StringBuilder("[");
while (h != null) { sb.append(h.val).append(h.next == null ? "" : ","); h = h.next; }
return sb.append("]").toString();
}
public static void main(String[] args) {
Solution s = new Solution();
ListNode h = new ListNode(1, new ListNode(4, new ListNode(3, new ListNode(2, new ListNode(5, new ListNode(2))))));
System.out.println(str(s.partition(h, 3))); // [1,2,2,4,3,5]
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1) --- 仅哨兵
87. Scramble String(扰乱字符串)
题目描述
使用下面描述的算法可以扰乱字符串 s 得到字符串 t:如果字符串长度为 1,算法停止;否则把字符串分成两个非空子串;交换或不交换两个子串后拼起来;对两个子串递归执行。给定两个长度相等的字符串 s1 和 s2,判断 s2 是否是 s1 的扰乱字符串。
示例:
输入:s1 = "great", s2 = "rgeat"
输出:true
输入:s1 = "abcde", s2 = "caebd"
输出:false
约束: s1.length == s2.length,1 <= s1.length <= 30,s1 和 s2 由小写英文字母组成
解题思路
核心思想:递归 + 记忆化------对每个分割点,(s1左,s2左) 且 (s1右,s2右) 都匹配,或 (s1左,s2右) 且 (s1右,s2左) 都匹配。
解法:递归 + 记忆化(推荐) 。dfs(s1的i1..i1+len, s2的i2..i2+len) 判断子串是否可扰乱。对长度 len 的每个分割点 k:①不交换:dfs(left,left) && dfs(right,right);②交换:dfs(s1左, s2右) && dfs(s1右, s2左)。用 memo[i1][i2][len] 记忆(Integer 三态)。剪枝:两子串字符集合不同直接 false。时间 O(n⁴),空间 O(n³)。
代码实现
java
class Solution {
private String s1, s2;
private int[][][] memo; // -1 未算,0 false,1 true
// 递归 + 记忆化(推荐)
public boolean isScramble(String s1, String s2) {
this.s1 = s1; this.s2 = s2;
int n = s1.length();
memo = new int[n][n][n + 1];
return dfs(0, 0, n);
}
private boolean dfs(int i1, int i2, int len) {
if (memo[i1][i2][len] != 0) return memo[i1][i2][len] == 1;
if (s1.substring(i1, i1 + len).equals(s2.substring(i2, i2 + len))) {
memo[i1][i2][len] = 1;
return true;
}
// 字符频率不一致直接剪枝
int[] cnt = new int[26];
for (int i = 0; i < len; i++) {
cnt[s1.charAt(i1 + i) - 'a']++;
cnt[s2.charAt(i2 + i) - 'a']--;
}
for (int c : cnt) if (c != 0) { memo[i1][i2][len] = -1; return false; }
// 尝试每个分割点
for (int k = 1; k < len; k++) {
if (dfs(i1, i2, k) && dfs(i1 + k, i2 + k, len - k)) { // 不交换
memo[i1][i2][len] = 1;
return true;
}
if (dfs(i1, i2 + len - k, k) && dfs(i1 + k, i2, len - k)) { // 交换
memo[i1][i2][len] = 1;
return true;
}
}
memo[i1][i2][len] = -1;
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isScramble("great", "rgeat")); // true
System.out.println(s.isScramble("abcde", "caebd")); // false
}
}
复杂度分析
- 时间复杂度:O(n⁴) --- 长度 × 起点组合 × 分割点
- 空间复杂度:O(n³) --- 记忆表
88. Merge Sorted Array(合并两个有序数组)
题目描述
给你两个按非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n,分别表示 nums1 和 nums2 中的元素数目。请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0,应忽略。nums2 的长度为 n。
示例:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
约束: 1 <= m + n <= 200,-10^9 <= nums1[i], nums2[j] <= 10^9
解题思路
核心思想:从后往前填充,避免覆盖 nums1 前部的元素。
解法:逆向双指针(推荐) 。i=m-1, j=n-1, k=m+n-1。比较 nums1[i] 与 nums2[j] 取较大者放 nums1[k--],直到 nums2 全部处理完。时间 O(m+n),空间 O(1)。
代码实现
java
import java.util.Arrays;
class Solution {
// 逆向双指针(推荐)
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m - 1, j = n - 1, k = m + n - 1;
while (j >= 0) {
if (i >= 0 && nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--]; // 取 nums1 较大者
} else {
nums1[k--] = nums2[j--]; // 取 nums2 较大者
}
}
}
public static void main(String[] args) {
Solution s = new Solution();
int[] a = {1,2,3,0,0,0};
s.merge(a, 3, new int[]{2,5,6}, 3);
System.out.println(Arrays.toString(a)); // [1,2,2,3,5,6]
}
}
复杂度分析
- 时间复杂度:O(m+n)
- 空间复杂度:O(1)
89. Gray Code(格雷编码)
题目描述
n 位格雷码序列是一个由 2^n 个整数组成的序列,其中:每个整数都在范围 [0, 2^n - 1] 内(含 0 和 2^n - 1);第一个整数是 0;一个整数序列在序列中不超过一次 ;每对相邻整数的二进制表示恰好一位不同,且第一个和最后一个整数的二进制表示恰好一位不同。给你一个整数 n,返回任一有效的 n 位格雷码序列。
示例:
输入:n = 2
输出:[0,1,3,2]
解释:[0,1,3,2] 的二进制表示是 [00,01,11,10],相邻恰好一位不同。
约束: 1 <= n <= 16
解题思路
核心思想 :格雷码构造公式------第 i 个格雷码 = i ^ (i >> 1)。
解法一:公式构造(推荐) 。直接对 0...2^n-1 逐个计算 i ^ (i >> 1) 生成。时间 O(2^n),空间 O(2^n)(结果)。
解法二:镜像反射递归。n 位格雷码 = 0 前缀的 (n-1) 位格雷码 + 1 前缀的逆序 (n-1) 位格雷码。适合讲解但实现略绕。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
// 公式构造:i ^ (i >> 1)(推荐)
public List<Integer> grayCode(int n) {
List<Integer> res = new ArrayList<Integer>();
int total = 1 << n;
for (int i = 0; i < total; i++) {
res.add(i ^ (i >> 1)); // 著名格雷码公式
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.grayCode(2)); // [0,1,3,2]
System.out.println(s.grayCode(1)); // [0,1]
}
}
复杂度分析
- 时间复杂度:O(2^n)
- 空间复杂度:O(2^n) --- 结果集
90. Subsets II(子集 II)
题目描述
给你一个整数数组 nums,其中可能包含重复元素 ,请你返回该数组所有可能的子集(幂集)。解集不能 包含重复的子集。返回的解集中,子集可以按任意顺序排列。
示例:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
约束: 1 <= nums.length <= 10,-10 <= nums[i] <= 10
解题思路
核心思想:与 78 题相同回溯,但排序 + 同层去重(与 40/47 题同一套路)。
解法:回溯 + 排序去重(推荐) 。先排序使相同值相邻。递归中:i > idx && nums[i] == nums[i-1] 跳过(同一层不要重复选择相同值)。时间 O(2^n),空间 O(n)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
private List<List<Integer>> res = new ArrayList<List<Integer>>();
// 回溯 + 排序去重(推荐)
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
dfs(nums, 0, new ArrayList<Integer>());
return res;
}
private void dfs(int[] nums, int idx, List<Integer> path) {
res.add(new ArrayList<Integer>(path)); // 每个状态都是子集
for (int i = idx; i < nums.length; i++) {
if (i > idx && nums[i] == nums[i - 1]) continue; // 同层去重
path.add(nums[i]);
dfs(nums, i + 1, path);
path.remove(path.size() - 1);
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.subsetsWithDup(new int[]{1,2,2})); // [[],[1],[1,2],[1,2,2],[2],[2,2]]
}
}
复杂度分析
- 时间复杂度:O(2^n)
- 空间复杂度:O(n) --- 递归栈
91. Decode Ways(解码方法)
题目描述
一条包含字母 A-Z 的消息通过以下映射进行了编码:'A' -> "1"、'B' -> "2" ... 'Z' -> "26"。要解码已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:"AAJF",将消息分组为 (1 1 10 6);"KJF",将消息分组为 (11 10 6)。注意,消息不能分组为 (1 11 06),因为 "06" 不能映射为 "F",这是由于 "6" 和 "06" 在映射中并不等价。给你一个只含数字的非空字符串 s,请计算并返回解码方法的总数。题目数据保证答案肯定是一个 32 位整数。
示例:
输入:s = "12" 输出:2("AB" 或 "L")
输入:s = "226" 输出:3("BZ"、"VF"、"BBF")
输入:s = "06" 输出:0("06" 无法解码)
约束: 1 <= s.length <= 100,s 只含数字且可能含前导零
解题思路
核心思想 :一维 DP------dp[i] 表示前 i 个字符的解码方案数,每步考虑"取 1 位"和"取 2 位"两种可能。
解法:一维 DP(推荐) 。dp[0]=1(空串)。对 i 从 1...n:若 s[i-1] != '0',dp[i] += dp[i-1](1 位数 1-9 可解码);若 i>=2 且 s[i-2..i-1] 在 10..26,dp[i] += dp[i-2]。可滚动只需前两个 dp 值。时间 O(n),空间 O(1)。
代码实现
java
class Solution {
// 一维 DP(推荐)
public int numDecodings(String s) {
int n = s.length();
int prev2 = 1, prev1 = 0; // prev2=dp[i-2], prev1=dp[i-1]
// 初始化 dp[1]
prev1 = (s.charAt(0) == '0') ? 0 : 1;
for (int i = 2; i <= n; i++) {
int cur = 0;
if (s.charAt(i - 1) != '0') cur += prev1; // 取 1 位 [1-9]
int two = (s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0');
if (two >= 10 && two <= 26) cur += prev2; // 取 2 位 [10-26]
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.numDecodings("12")); // 2
System.out.println(s.numDecodings("226")); // 3
System.out.println(s.numDecodings("06")); // 0
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1) --- 滚动变量
92. Reverse Linked List II(反转链表 II)
题目描述
给你单链表的头指针 head 和两个整数 left 和 right,其中 left <= right。请你反转从位置 left 到位置 right 的链表节点,返回反转后的链表。
示例:
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
约束: 链表中节点数目为 n,1 <= n <= 500,-500 <= Node.val <= 500,1 <= left <= right <= n
解题思路
核心思想:头插法------定位 left 前驱,把区间内节点逐个"头插"到区间头部,实现区间反转。
解法:头插法(推荐) 。dummy 哨兵;pre 走到 left 的前驱;cur = pre.next。执行 right-left 次:把 cur.next 摘出来插到 pre.next,cur 不动保持指向段内节点。时间 O(n),空间 O(1)。
代码实现
java
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
// 头插法(推荐)
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(0, head);
ListNode pre = dummy;
for (int i = 1; i < left; i++) pre = pre.next; // 走到 left 前驱
ListNode cur = pre.next;
for (int i = left; i < right; i++) {
ListNode nxt = cur.next; // 待头插的节点
cur.next = nxt.next; // 摘除
nxt.next = pre.next; // 头插
pre.next = nxt;
}
return dummy.next;
}
private static ListNode build(int[] arr) {
ListNode d = new ListNode(0), c = d;
for (int v : arr) { c.next = new ListNode(v); c = c.next; }
return d.next;
}
private static String str(ListNode h) {
StringBuilder sb = new StringBuilder("[");
while (h != null) { sb.append(h.val).append(h.next == null ? "" : ","); h = h.next; }
return sb.append("]").toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(str(s.reverseBetween(build(new int[]{1,2,3,4,5}), 2, 4))); // [1,4,3,2,5]
System.out.println(str(s.reverseBetween(build(new int[]{5}), 1, 1))); // [5]
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
93. Restore IP Addresses(复原 IP 地址)
题目描述
有效 IP 地址正好由四个整数(每个位于 0 到 255 之间组成,且不能含有前导零)构成。例如,"0.1.2.201" 和 "192.168.1.1" 是有效 IP 地址;除了数字 0 本身,其他数字不能含前导零。给你一个只含数字的字符串 s,表示一个 IP 地址,返回所有可能的有效 IP 地址 ,这些地址可以通过在 s 中插入 '.' 来形成。你不能重新排序或删除 s 中的任何数字,也不能以任何方式重新排序。
示例:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
约束: 1 <= s.length <= 20,s 仅由数字组成
解题思路
核心思想:回溯切 4 段------每段取 1~3 位并校验合法(0-255、无前导零)。
解法:回溯(推荐) 。dfs(start, seg, path):当前已切 seg 段、从 start 开始选下一段。校验 isValid(段):长度 1-3、无前导零(除非是 "0")、值 ≤ 255。切满 4 段且用完全部字符时记录。时间 O(3^4)=O(1)(最多 4 段每段 3 种长度),空间 O(1)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
private List<String> res = new ArrayList<String>();
// 回溯(推荐)
public List<String> restoreIpAddresses(String s) {
dfs(s, 0, 0, new StringBuilder());
return res;
}
private void dfs(String s, int start, int seg, StringBuilder sb) {
if (seg == 4) { // 已切 4 段
if (start == s.length()) res.add(sb.toString());
return;
}
for (int len = 1; len <= 3 && start + len <= s.length(); len++) {
String part = s.substring(start, start + len);
if (!isValid(part)) continue;
int oldLen = sb.length();
sb.append(part);
if (seg < 3) sb.append('.'); // 非最后一段加分隔点
dfs(s, start + len, seg + 1, sb);
sb.setLength(oldLen); // 撤销
}
}
private boolean isValid(String part) {
if (part.length() > 1 && part.charAt(0) == '0') return false; // 前导零
return Integer.parseInt(part) <= 255;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.restoreIpAddresses("25525511135")); // [255.255.11.135, 255.255.111.35]
System.out.println(s.restoreIpAddresses("0000")); // [0.0.0.0]
}
}
复杂度分析
- 时间复杂度:O(3^4) --- 固定常数上界
- 空间复杂度:O(1) --- 递归深度 4
94. Binary Tree Inorder Traversal(二叉树的中序遍历)
题目描述
给定一个二叉树的根节点 root,返回它的中序遍历。中序遍历顺序:左子树 → 根 → 右子树。
示例:
输入:root = [1,null,2,3]
输出:[1,3,2]
约束: 树中节点数目在范围 [0, 100] 内,-100 <= Node.val <= 100
解题思路
核心思想:中序 = 左-根-右。可递归或显式栈迭代(Morris 可做到 O(1) 空间)。
解法一:递归(推荐) 。inorder(root.left) → 记录 root → inorder(root.right)。时间 O(n),空间 O(h)(h 为树高,递归栈)。
解法二:显式栈迭代。不断向左压栈,弹栈访问后转右子树。时间 O(n),空间 O(h)。
解法三:Morris 遍历 。利用线索指针,时间 O(n)、空间 O(1)。完整可运行文件 放在 94/Solution.java(含类 TreeNode)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
import java.util.ArrayDeque;
import java.util.Deque;
// 树节点定义(LeetCode 预置,提交时勿粘贴)
class TreeNode {
int val;
TreeNode left, right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}
class Solution {
// 解法一:递归
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
inorder(root, res);
return res;
}
private void inorder(TreeNode node, List<Integer> res) {
if (node == null) return;
inorder(node.left, res); // 左
res.add(node.val); // 根
inorder(node.right, res); // 右
}
// 解法二:显式栈迭代
public List<Integer> inorderTraversalIter(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) { stack.push(cur); cur = cur.left; } // 左入栈
cur = stack.pop();
res.add(cur.val); // 访问根
cur = cur.right; // 转右
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
TreeNode root = new TreeNode(1, null, new TreeNode(2, new TreeNode(3), null));
System.out.println(s.inorderTraversal(root)); // [1,3,2]
System.out.println(s.inorderTraversalIter(root)); // [1,3,2]
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 递归(推荐) | O(n) | O(h) | 最直观 |
| 显式栈 | O(n) | O(h) | 避免递归栈溢出 |
| Morris | O(n) | O(1) | 线索指针,进阶 |
95. Unique Binary Search Trees II(不同的二叉搜索树 II)
题目描述
给你一个整数 n,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同二叉搜索树。可以按任意顺序返回答案。
示例:
输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]
约束: 1 <= n <= 8
解题思路
核心思想:递归分治+回溯------以每个数为根,左右子树分别是比它小/大的数能形成的所有 BST 组合。
解法:递归构造(推荐) 。generate(start, end):对区间 start, end 内每个 i 作为根,递归生成左子树集合(start...i-1)与右子树集合(i+1...end),然后两两交叉组合所有 (左, 根, 右)。空区间返回含 null 的列表。时间 O(卡特兰数),空间 O(卡特兰数 × n)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
// 树节点定义(LeetCode 预置,提交时勿粘贴)
class TreeNode {
int val;
TreeNode left, right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}
class Solution {
// 递归分治(推荐)
public List<TreeNode> generateTrees(int n) {
return build(1, n);
}
private List<TreeNode> build(int start, int end) {
List<TreeNode> res = new ArrayList<TreeNode>();
if (start > end) { res.add(null); return res; } // 空子树
for (int i = start; i <= end; i++) {
List<TreeNode> leftList = build(start, i - 1); // 左子树候选
List<TreeNode> rightList = build(i + 1, end); // 右子树候选
for (TreeNode left : leftList) {
for (TreeNode right : rightList) {
res.add(new TreeNode(i, left, right)); // 组合成树
}
}
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.generateTrees(3).size()); // 5
System.out.println(s.generateTrees(1).size()); // 1
}
}
复杂度分析
- 时间复杂度:O(4ⁿ/√n) --- 卡特兰数级
- 空间复杂度:O(4ⁿ/√n × n) --- 结果集
96. Unique Binary Search Trees(不同的二叉搜索树)
题目描述
给你一个整数 n,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的二叉搜索树有多少种?返回满足题意的二叉搜索树的种数。
示例:
输入:n = 3
输出:5
约束: 1 <= n <= 19
解题思路
核心思想 :卡特兰数------G(n) = Σ_{i=1..n} G(i-1) * G(n-i)。
解法一:DP(推荐) 。dp[i] 表示 i 个节点能构成的 BST 数量(节点值只影响结构不影响数量)。对每个根 j,dp[i] += dp[j-1] * dp[i-j]。时间 O(n²),空间 O(n)。
解法二:公式 。G(n) = C(2n, n) / (n+1),用 long 计算组合数。时间 O(n),空间 O(1)。
代码实现
java
class Solution {
// 解法一:DP(推荐)
public int numTrees(int n) {
int[] dp = new int[n + 1];
dp[0] = 1; // 空树
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
dp[i] += dp[j - 1] * dp[i - j]; // 左子树 j-1 个,右子树 i-j 个
}
}
return dp[n];
}
// 解法二:卡特兰公式 C(2n,n)/(n+1)
public int numTreesFormula(int n) {
long c = 1; // C(2n, n) 递推
for (int i = 0; i < n; i++) {
c = c * 2 * (2 * i + 1) / (i + 2); // 卡塔兰递推 c_(i+1)
}
return (int) c;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.numTrees(3)); // 5
System.out.println(s.numTreesFormula(3)); // 5
System.out.println(s.numTrees(19)); // 1767263190
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| DP(推荐) | O(n²) | O(n) | 递推清晰 |
| 卡特兰公式 | O(n) | O(1) | 公式迭代 |
97. Interleaving String(交错字符串)
题目描述
给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错组成的。交错是指交替使用 s1 和 s2 中的字符(原顺序保持),例如 s1="aabcc",s2="dbbca",交错结果之一为 "aadbbcbcac"。
示例:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false
约束: 0 <= s1.length, s2.length <= 100,0 <= s3.length <= 200,s1、s2、s3 由小写英文字母组成
解题思路
核心思想 :双串 DP------dp[i][j] 表示 s3 前 i+j 个字符能否由 s1 前 i 个 + s2 前 j 个交错组成。
解法:二维 DP(推荐) 。先判长度 len1+len2 == len3。转移:dp[i][j] 为 true 当 (i>0 && dp[i-1][j] && s3[i+j-1]==s1[i-1]) || (j>0 && dp[i][j-1] && s3[i+j-1]==s2[j-1])。时间 O(m×n),空间 O(m×n)(可滚动 O(n))。
代码实现
java
class Solution {
// 二维 DP(推荐)
public boolean isInterleave(String s1, String s2, String s3) {
int m = s1.length(), n = s2.length();
if (m + n != s3.length()) return false;
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
int k = i + j - 1; // 对应 s3 下标
if (i > 0 && dp[i - 1][j] && s3.charAt(k) == s1.charAt(i - 1)) dp[i][j] = true;
if (j > 0 && dp[i][j - 1] && s3.charAt(k) == s2.charAt(j - 1)) dp[i][j] = true;
}
}
return dp[m][n];
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isInterleave("aabcc", "dbbca", "aadbbcbcac")); // true
System.out.println(s.isInterleave("aabcc", "dbbca", "aadbbbaccc")); // false
}
}
复杂度分析
- 时间复杂度:O(m×n)
- 空间复杂度:O(m×n)(可滚动优化为 O(n))
98. Validate Binary Search Tree(验证二叉搜索树)
题目描述
给你一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树。有效二叉搜索树定义如下:节点的左子树只包含小于 当前节点的数;节点的右子树只包含大于当前节点的数;所有左子树和右子树自身必须也是二叉搜索树。
示例:
输入:root = [2,1,3]
输出:true
输入:root = [5,1,4,null,null,3,6]
输出:false
约束: 树中节点数目范围在 [1, 10^4] 内,-2^31 <= Node.val <= 2^31 - 1
解题思路
核心思想:中序遍历应严格递增;或用区间约束递归校验(用 long 规避 int 边界)。
解法一:区间约束递归(推荐) 。dfs(node, low, high):节点值必须满足 low < val < high,左子树用 (low, val),右子树用 (val, high)。用 Long 类型上下界处理 int 边界。时间 O(n),空间 O(h)。
解法二:中序遍历。中序序列必须严格递增,迭代版记录 prev。时间 O(n),空间 O(h)。
代码实现
java
class TreeNode {
int val;
TreeNode left, right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}
class Solution {
// 解法一:区间约束递归(推荐)
public boolean isValidBST(TreeNode root) {
return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean dfs(TreeNode node, long low, long high) {
if (node == null) return true;
if (node.val <= low || node.val >= high) return false; // 超出区间
// 左子树收紧上界,右子树收紧下界
return dfs(node.left, low, node.val) && dfs(node.right, node.val, high);
}
// 解法二:中序遍历判断递增
private Integer prev = null;
public boolean isValidBSTInorder(TreeNode root) {
return inorder(root);
}
private boolean inorder(TreeNode node) {
if (node == null) return true;
if (!inorder(node.left)) return false;
if (prev != null && node.val <= prev) return false; // 必须严格大于
prev = node.val;
return inorder(node.right);
}
public static void main(String[] args) {
Solution s = new Solution();
TreeNode t1 = new TreeNode(2, new TreeNode(1), new TreeNode(3));
System.out.println(s.isValidBST(t1)); // true
TreeNode t2 = new TreeNode(5, new TreeNode(1), new TreeNode(4, new TreeNode(3), new TreeNode(6)));
System.out.println(s.isValidBST(t2)); // false
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 区间约束(推荐) | O(n) | O(h) | 传入高低界 |
| 中序遍历 | O(n) | O(h) | 依赖"严格递增"性质 |
99. Recover Binary Search Tree(恢复二叉搜索树)
题目描述
给你二叉搜索树的根节点 root,该树中的恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树。进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O(1) 空间的解决方案吗?
示例:
输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2](恢复后 3 和 1 交换)
约束: 树上节点数目在范围 [2, 1000] 内,-2^31 <= Node.val <= 2^31 - 1
解题思路
核心思想:BST 中序遍历应递增;恰好两个节点错位会在序列中形成 1~2 组"前大后小"。用中序遍历找出这两处,交换第一个大值和最后一个小值。
解法:中序遍历 + 交换(推荐) 。迭代中序(或递归),记录 prev。当出现 node.val < prev.val 时:第一次记录 first=prev,第二次记录 second=node,最后交换 first 与 second 的 val(若只有一组错位,second 就是那个后点)。时间 O(n),空间 O(h)(Morris 可做到 O(1))。
代码实现
java
class TreeNode {
int val;
TreeNode left, right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}
class Solution {
private TreeNode prev = null, first = null, second = null;
// 中序遍历定位错位节点(推荐)
public void recoverTree(TreeNode root) {
prev = null; first = null; second = null;
inorder(root);
// 交换两个错位节点的值
int tmp = first.val;
first.val = second.val;
second.val = tmp;
}
private void inorder(TreeNode node) {
if (node == null) return;
inorder(node.left);
if (prev != null && node.val < prev.val) { // 出现逆序
if (first == null) first = prev; // 第一处:记录前点
second = node; // 第二处(或同一处):记录后点
}
prev = node;
inorder(node.right);
}
private static String serialize(TreeNode r) {
if (r == null) return "null";
return r.val + "," + serialize(r.left) + "," + serialize(r.right);
}
public static void main(String[] args) {
Solution s = new Solution();
TreeNode root = new TreeNode(3, new TreeNode(1), new TreeNode(2)); // 3,2 错位(应为1,2,3)
s.recoverTree(root);
System.out.println(serialize(root)); // 2,1,3 结构应等价于合法 BST
}
}
复杂度分析
- 时间复杂度:O(n) --- 单次中序遍历
- 空间复杂度:O(h) --- 递归栈(用 Morris 可 O(1))
100. Same Tree(相同的树)
题目描述
给你两棵二叉树的根节点 p 和 q,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例:
输入:p = [1,2,3], q = [1,2,3]
输出:true
输入:p = [1,2], q = [1,null,2]
输出:false
约束: 两棵树上的节点数目都在范围 [0, 100] 内,-10^4 <= Node.val <= 10^4
解题思路
核心思想:递归比较------两节点都空则 true,一空一非空则 false,值不等则 false,否则递归比较左右子树。
解法一:递归(推荐) 。isSameTree(p,q):三方判断同值 + 左左递归 + 右右递归。时间 O(n),空间 O(h)。
解法二:迭代 BFS。用队列同时入队两棵树节点,成对取出比较。时间 O(n),空间 O(n)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
class TreeNode {
int val;
TreeNode left, right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}
class Solution {
// 解法一:递归(推荐)
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) return true; // 均空
if (p == null || q == null) return false; // 一空
if (p.val != q.val) return false; // 值不同
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); // 递归子树
}
// 解法二:迭代 BFS
public boolean isSameTreeIter(TreeNode p, TreeNode q) {
Deque<TreeNode> queue = new ArrayDeque<TreeNode>();
queue.offer(p); queue.offer(q);
while (!queue.isEmpty()) {
TreeNode a = queue.poll(), b = queue.poll();
if (a == null && b == null) continue;
if (a == null || b == null || a.val != b.val) return false;
queue.offer(a.left); queue.offer(b.left);
queue.offer(a.right); queue.offer(b.right);
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
TreeNode p = new TreeNode(1, new TreeNode(2), new TreeNode(3));
TreeNode q = new TreeNode(1, new TreeNode(2), new TreeNode(3));
System.out.println(s.isSameTree(p, q)); // true
TreeNode p2 = new TreeNode(1, new TreeNode(2), null);
TreeNode q2 = new TreeNode(1, null, new TreeNode(2));
System.out.println(s.isSameTree(p2, q2)); // false
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 递归(推荐) | O(n) | O(h) | 最直观 |
| 迭代 BFS | O(n) | O(n) | 队列宽度 |