代码随想录题单题解笔记(Java)· 中
本文是代码随想录笔记中篇(二叉树 / 回溯 / 贪心)。
接上篇。本篇覆盖二叉树、回溯与贪心。
目录
7. 二叉树 (Binary Tree)
核心思想: 遍历(递归/迭代/层序)、属性、构造、BST 专题。
题目列表
144. 二叉树的前序遍历 (Binary Tree Preorder Traversal) 简单 ✅ 2026-08-15 标签:栈, 树, 深度优先搜索, 二叉树
145. 二叉树的后序遍历 (Binary Tree Postorder Traversal) 简单 标签:栈, 树, 深度优先搜索, 二叉树
94. 二叉树的中序遍历 (Binary Tree Inorder Traversal) 简单 标签:栈, 树, 深度优先搜索, 二叉树
102. 二叉树的层序遍历 (Binary Tree Level Order Traversal) 中等 标签:树, 广度优先搜索, 二叉树
107. 二叉树的层序遍历 II (Binary Tree Level Order Traversal II) 中等 标签:树, 广度优先搜索, 二叉树
199. 二叉树的右视图 (Binary Tree Right Side View) 中等 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
637. 二叉树的层平均值 (Average of Levels in Binary Tree) 简单 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
429. N 叉树的层序遍历 (N-ary Tree Level Order Traversal) 中等 标签:树, 广度优先搜索
515. 在每个树行中找最大值 (Find Largest Value in Each Tree Row) 中等 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
116. 填充每个节点的下一个右侧节点指针 (Populating Next Right Pointers in Each Node) 中等 标签:树, 深度优先搜索, 广度优先搜索, 链表, 二叉树
117. 填充每个节点的下一个右侧节点指针 II (Populating Next Right Pointers in Each Node II) 中等 标签:树, 深度优先搜索, 广度优先搜索, 链表, 二叉树
104. 二叉树的最大深度 (Maximum Depth of Binary Tree) 简单 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
111. 二叉树的最小深度 (Minimum Depth of Binary Tree) 简单 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
226. 翻转二叉树 (Invert Binary Tree) 简单 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
101. 对称二叉树 (Symmetric Tree) 简单 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
222. 完全二叉树的节点个数 (Count Complete Tree Nodes) 简单 标签:位运算, 树, 二分查找, 二叉树
110. 平衡二叉树 (Balanced Binary Tree) 简单 标签:树, 深度优先搜索, 二叉树
257. 二叉树的所有路径 (Binary Tree Paths) 简单 标签:树, 深度优先搜索, 字符串, 回溯, 二叉树
404. 左叶子之和 (Sum of Left Leaves) 简单 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
513. 找树左下角的值 (Find Bottom Left Tree Value) 中等 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
112. 路径总和 (Path Sum) 简单 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
105. 从前序与中序遍历序列构造二叉树 (Construct Binary Tree from Preorder and Inorder Traversal) 中等 标签:树, 数组, 哈希表, 分治, 二叉树
106. 从中序与后序遍历序列构造二叉树 (Construct Binary Tree from Inorder and Postorder Traversal) 中等 标签:树, 数组, 哈希表, 分治, 二叉树
654. 最大二叉树 (Maximum Binary Tree) 中等 标签:栈, 树, 数组, 分治, 二叉树, 单调栈
617. 合并二叉树 (Merge Two Binary Trees) 简单 标签:树, 深度优先搜索, 广度优先搜索, 二叉树
700. 二叉搜索树中的搜索 (Search in a Binary Search Tree) 简单 标签:树, 二叉搜索树, 二叉树
98. 验证二叉搜索树 (Validate Binary Search Tree) 中等 标签:树, 深度优先搜索, 二叉搜索树, 二叉树
530. 二叉搜索树的最小绝对差 (Minimum Absolute Difference in BST) 简单 标签:树, 深度优先搜索, 广度优先搜索, 二叉搜索树, 二叉树
501. 二叉搜索树中的众数 (Find Mode in Binary Search Tree) 简单 标签:树, 深度优先搜索, 二叉搜索树, 二叉树
236. 二叉树的最近公共祖先 (Lowest Common Ancestor of a Binary Tree) 中等 标签:树, 深度优先搜索, 二叉树
235. 二叉搜索树的最近公共祖先 (Lowest Common Ancestor of a Binary Search Tree) 中等 标签:树, 深度优先搜索, 二叉搜索树, 二叉树
701. 二叉搜索树中的插入操作 (Insert into a Binary Search Tree) 中等 标签:树, 二叉搜索树, 二叉树
450. 删除二叉搜索树中的节点 (Delete Node in a BST) 中等 标签:树, 二叉搜索树, 二叉树
669. 修剪二叉搜索树 (Trim a Binary Search Tree) 中等 标签:树, 深度优先搜索, 二叉搜索树, 二叉树
108. 将有序数组转换为二叉搜索树 (Convert Sorted Array to Binary Search Tree) 简单 标签:树, 二叉搜索树, 数组, 分治, 二叉树
538. 把二叉搜索树转换为累加树 (Convert BST to Greater Tree) 中等 标签:树, 深度优先搜索, 二叉搜索树, 二叉树
144. 二叉树的前序遍历 (Binary Tree Preorder Traversal)
栈树深度优先搜索二叉树简单
题目
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,2,3]
示例 2:
输入:root = [1,2,3,4,5,null,8,null,null,6,7,9]
输出:[1,2,4,5,6,7,3,8,9]
示例 3:
输入:root = []
输出:[]
示例 4:
输入:root = [1]
输出:[1]
提示:
- 树中节点数目在范围
[0, 100]内 -100 <= Node.val <= 100- 进阶:递归算法很简单,你可以通过迭代算法完成吗?
思路简述
前序顺序为「根 → 左 → 右」。递归最直观;迭代用栈模拟;Morris 借空指针建临时线索,额外空间 O(1):有左子树时找左子树最右结点,第一次访问当前根并线索指向根再走左,第二次拆除线索后走右。
参考代码(解法一:Morris,O(1) 额外空间)
JAVA
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
while (root != null) {
if (root.left != null) {
// 找 root 的前驱 pre:左子树里「最右」的结点
// 从 root.left 一直向右,直到尽头,或遇到已指向 root 的线索
TreeNode pre = root.left;
while (pre.right != null && pre.right != root) {
pre = pre.right;
}
// 左子树尚未访问:先记录根(前序),再建立线索并走左
if (pre.right == null) {
ans.add(root.val); // 前序:第一次到达就访问根(与中序的差别)
pre.right = root; // 建立线索(回到 root 的路),相当于把 pre.right 当栈
root = root.left;
continue;
}
// 左子树已访问完:拆掉线索,恢复原样
pre.right = null; // 若调用后不再使用该树,这行可省略
root = root.right;
} else {
// 无左子树:访问根,再走右
ans.add(root.val);
root = root.right;
}
}
return ans;
}
}
复杂度(Morris)
- 时间:O(n)
- 空间:O(1) 额外(不计答案;临时改指针,结束前已恢复)
参考代码(解法二:栈迭代)
JAVA
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
if (root == null) {
return ans;
}
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
ans.add(node.val); // 先访问根
// 先压右再压左,保证弹出时左先于右
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
return ans;
}
}
复杂度(迭代)
- 时间:O(n)
- 空间:O(h),h 为树高
参考代码(解法三:递归)
JAVA
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
preOrder(root, ans);
return ans;
}
private void preOrder(TreeNode node, List<Integer> ans) {
if (node == null) {
return;
}
ans.add(node.val); // 根
preOrder(node.left, ans); // 左
preOrder(node.right, ans); // 右
}
}
复杂度(递归)
- 时间:O(n)
- 空间:O(h),递归栈
145. 二叉树的后序遍历 (Binary Tree Postorder Traversal)
栈树深度优先搜索二叉树简单
题目
给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[3,2,1]
示例 2:
输入:root = [1,2,3,4,5,null,8,null,null,6,7,9]
输出:[4,6,7,5,2,9,8,3,1]
示例 3:
输入:root = []
输出:[]
示例 4:
输入:root = [1]
输出:[1]
提示:
- 树中节点的数目在范围
[0, 100]内 -100 <= Node.val <= 100- 进阶:递归算法很简单,你可以通过迭代算法完成吗?
思路简述
后序顺序为「左 → 右 → 根」。递归最直观;迭代可先做「根 → 右 → 左」再反转;Morris 同理:对右子树建线索做镜像前序(根右左),最后 Collections.reverse,额外空间 O(1)(不计答案)。
参考代码(解法一:Morris,O(1) 额外空间)
JAVA
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
// 镜像前序:根 → 右 → 左,最后整体反转即后序
while (root != null) {
if (root.right != null) {
// 找「后继」:右子树里最左结点,用来挂回到 root 的线索
TreeNode pre = root.right;
while (pre.left != null && pre.left != root) {
pre = pre.left;
}
// 右子树尚未访问:先记录根,再建线索并走右
if (pre.left == null) {
ans.add(root.val);
pre.left = root; // 线索:走完右子树后回到 root
root = root.right;
continue;
}
// 右子树已访问完:拆线索,走左
pre.left = null;
root = root.left;
} else {
// 无右子树:访问根,再走左
ans.add(root.val);
root = root.left;
}
}
Collections.reverse(ans); // 根右左 → 左右根
return ans;
}
}
复杂度(Morris)
- 时间:O(n)
- 空间:O(1) 额外(不计答案与反转;临时改指针,结束前已恢复)
参考代码(解法二:栈迭代)
JAVA
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
if (root == null) {
return ans;
}
// 先得到「根 → 右 → 左」,再反转
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
ans.add(node.val);
// 先压左再压右,弹出时右先于左
if (node.left != null) {
stack.push(node.left);
}
if (node.right != null) {
stack.push(node.right);
}
}
Collections.reverse(ans);
return ans;
}
}
复杂度(迭代)
- 时间:O(n)
- 空间:O(h)
参考代码(解法三:递归)
JAVA
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
postOrder(root, ans);
return ans;
}
private void postOrder(TreeNode node, List<Integer> ans) {
if (node == null) {
return;
}
postOrder(node.left, ans); // 左
postOrder(node.right, ans); // 右
ans.add(node.val); // 根
}
}
复杂度(递归)
- 时间:O(n)
- 空间:O(h),递归栈
94. 二叉树的中序遍历 (Binary Tree Inorder Traversal)
栈树深度优先搜索二叉树简单
题目
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示:
- 树中节点数目在范围
[0, 100]内 -100 <= Node.val <= 100- 进阶: 递归算法很简单,你可以通过迭代算法完成吗?
思路简述
中序顺序为「左 → 根 → 右」。递归最直观;迭代用栈沿左链下压、弹出访问再转右;Morris:有左子树时找前驱,第一次只建线索走左,第二次拆线索后访问根再走右(与前序「第一次就访问」相对)。
参考代码(解法一:Morris,O(1) 额外空间)
JAVA
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
while (root != null) {
if (root.left != null) {
// 找 root 的前驱 pre:在中序遍历中,root 的上一个节点
// 从 root.left 开始,一直向右走,直到走到尽头,或者遇到指向 root 的线索
TreeNode pre = root.left;
while (pre.right != null && pre.right != root) {
pre = pre.right;
}
// root 的左子树尚未访问
if (pre.right == null) {
pre.right = root; // 建立线索(回到 root 的路),相当于把 pre.right 当作栈
root = root.left; // 访问左子树
continue;
}
// root 的左子树访问完毕,去掉线索,恢复原样
pre.right = null; // 若调用后不再使用该树,这行可省略
}
// 无左子树,或左子树已访问完:记录当前节点,再走右
ans.add(root.val);
root = root.right;
}
return ans;
}
}
复杂度(Morris)
- 时间:O(n)
- 空间:O(1) 额外(不计答案;临时改指针,结束前已恢复)
参考代码(解法二:栈迭代)
JAVA
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left; // 一路向左
}
cur = stack.pop(); // 左空:访问根
ans.add(cur.val);
cur = cur.right; // 再处理右子树
}
return ans;
}
}
复杂度(迭代)
- 时间:O(n)
- 空间:O(h)
参考代码(解法三:递归)
JAVA
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
inOrder(root, ans);
return ans;
}
private void inOrder(TreeNode node, List<Integer> ans) {
if (node == null) {
return;
}
inOrder(node.left, ans); // 左
ans.add(node.val); // 根
inOrder(node.right, ans); // 右
}
}
复杂度(递归)
- 时间:O(n)
- 空间:O(h),递归栈
102. 二叉树的层序遍历 (Binary Tree Level Order Traversal)
树广度优先搜索二叉树中等
题目
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:1
示例 3:
输入:root = []
输出:[]
提示:
- 树中节点数目在范围
[0, 2000]内 -1000 <= Node.val <= 1000
思路简述
BFS:队列中先放根;每一层先记下当前 size,连续取出这么多结点写入本层列表,并把其子结点入队。也可用「当前层队列 / 下一层队列」交替,复杂度相同。
双队列(curr / next)与单队列记 size 等价;后者少一个队列对象,更常见。
参考代码
JAVA
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<>();
if (root == null) {
return ans;
}
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size(); // 本层结点个数
List<Integer> level = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
TreeNode cur = queue.poll();
level.add(cur.val);
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
ans.add(level);
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(n),队列最宽一层
107. 二叉树的层序遍历 II (Binary Tree Level Order Traversal II)
树广度优先搜索二叉树中等
题目
给你二叉树的根节点 root ,返回其节点值 自底向上的层序遍历 。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[15,7],[9,20],[3]]
示例 2:
输入:root = [1]
输出:1
示例 3:
输入:root = []
输出:[]
提示:
- 树中节点数目在范围
[0, 2000]内 -1000 <= Node.val <= 1000
思路简述
普通 BFS 层序 自顶向下收集,最后 Collections.reverse 得到自底向上。相对逐层 ArrayList.addFirst,反转整表是 O(层数),不会退化成 O(层数²)。
ArrayList.addFirst / add(0, x) 每次要搬移已有元素,层数多时偏慢;用 Collections.reverse 或 LinkedList/ArrayDeque 头插。
参考代码
JAVA
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> ans = new ArrayList<>();
if (root == null) {
return ans;
}
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size(); // 当前层节点数
List<Integer> level = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
TreeNode cur = queue.poll();
level.add(cur.val);
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
ans.add(level); // 先自顶向下收集
}
Collections.reverse(ans); // 再整体反转 → 自底向上
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(n),队列与答案
199. 二叉树的右视图 (Binary Tree Right Side View)
树深度优先搜索广度优先搜索二叉树中等
题目
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例 1:
输入:root = [1,2,3,null,5,null,4]
输出:[1,3,4]
示例 2:
输入:root = [1,2,3,4,null,null,null,5]
输出:[1,3,4,5]
示例 3:
输入:root = [1,null,3]
输出:[1,3]
示例 4:
输入:root = []
输出:[]
提示:
- 二叉树的节点个数的范围是
[0,100] -100 <= Node.val <= 100
思路简述
每一层最右侧的结点。可用 BFS:取每层最后一个出队结点。也可用 DFS:先右后左,每个深度第一次到达的结点即右视图;额外空间 O(h),通常更优。
参考代码(解法一:DFS,空间更优)
JAVA
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> ans = new ArrayList<>();
dfs(root, 0, ans);
return ans;
}
// depth:当前深度;ans.size() 表示已记录到的最大深度+1
private void dfs(TreeNode node, int depth, List<Integer> ans) {
if (node == null) {
return;
}
if (depth == ans.size()) {
ans.add(node.val); // 该深度首次到达 = 最右侧
}
dfs(node.right, depth + 1, ans); // 先右
dfs(node.left, depth + 1, ans);
}
}
复杂度(DFS)
- 时间:O(n)
- 空间:O(h),递归栈
参考代码(解法二:BFS)
JAVA
class Solution {
public List<Integer> rightSideView(TreeNode root) {
List<Integer> ans = new ArrayList<>();
if (root == null) {
return ans;
}
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
if (i == size - 1) {
ans.add(node.val); // 本层最后一个 = 右视图
}
}
}
return ans;
}
}
复杂度(BFS)
- 时间:O(n)
- 空间:O(n),队列最宽一层
637. 二叉树的层平均值 (Average of Levels in Binary Tree)
树深度优先搜索广度优先搜索二叉树简单
题目
给定一个非空二叉树的根节点 root , 以数组的形式返回每一层节点的平均值。与实际答案相差 10-5 以内的答案可以被接受。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[3.00000,14.50000,11.00000]
解释:第 0 层的平均值为 3,第 1 层的平均值为 14.5,第 2 层的平均值为 11 。 因此返回 [3, 14.5, 11] 。
示例 2:
输入:root = [3,9,20,15,7]
输出:[3.00000,14.50000,11.00000]
提示:
- 树中节点数量在
[1, 10^4]范围内 -231 <= Node.val <= 231 - 1
思路简述
DFS 按深度累加:levelSum[d]、levelCnt[d] 分别记第 d 层的和与个数,最后相除得均值。和用 long 防溢出。相对 BFS,额外空间 O(h) 更优。
参考代码
JAVA
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Long> levelSum = new ArrayList<>();
List<Integer> levelCnt = new ArrayList<>();
dfs(root, 0, levelSum, levelCnt);
List<Double> ans = new ArrayList<>(levelCnt.size());
for (int i = 0; i < levelCnt.size(); i++) {
ans.add(levelSum.get(i) * 1.0 / levelCnt.get(i));
}
return ans;
}
private void dfs(TreeNode node, int depth, List<Long> levelSum, List<Integer> levelCnt) {
if (node == null) {
return;
}
if (depth == levelCnt.size()) {
// 第一次到达该深度:新建一层
levelCnt.add(1);
levelSum.add((long) node.val);
} else {
levelCnt.set(depth, levelCnt.get(depth) + 1);
levelSum.set(depth, levelSum.get(depth) + node.val);
}
dfs(node.left, depth + 1, levelSum, levelCnt);
dfs(node.right, depth + 1, levelSum, levelCnt);
}
}
复杂度
- 时间:O(n)
- 空间:O(h),递归栈与按层数组(层数 ≤ h)
429. N 叉树的层序遍历 (N-ary Tree Level Order Traversal)
树广度优先搜索中等
题目
给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。
树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。
示例 1:
输入:root = [1,null,3,2,4,null,5,6]
输出:[[1],[3,2,4],[5,6]]
示例 2:
输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]
提示:
- 树的高度不会超过
1000 - 树的节点总数在
[0, 10^4]之间
思路简述
与二叉树层序相同:BFS 按层取 size,将当前结点的 children 全部入队即可。
参考代码
JAVA
class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> ans = new ArrayList<>();
if (root == null) {
return ans;
}
Queue<Node> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> level = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
Node cur = queue.poll();
level.add(cur.val);
// N 叉:所有孩子依次入队
for (Node child : cur.children) {
queue.offer(child);
}
}
ans.add(level);
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(n),队列最宽一层
515. 在每个树行中找最大值 (Find Largest Value in Each Tree Row)
树深度优先搜索广度优先搜索二叉树中等
题目
给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。
示例 1:
输入:root = [1,3,2,5,3,null,9]
输出:[1,3,9]
示例 2:
输入:root = [1,2,3]
输出:[1,3]
提示:
- 二叉树的节点个数的范围是
[0,104] -231 <= Node.val <= 231 - 1
思路简述
BFS 按层:每层遍历时维护 max,层结束写入答案。也可用 DFS 按深度更新 ans.get(depth)。
参考代码
JAVA
class Solution {
public List<Integer> largestValues(TreeNode root) {
List<Integer> ans = new ArrayList<>();
if (root == null) {
return ans;
}
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
int max = Integer.MIN_VALUE; // 本层最大值
for (int i = 0; i < size; i++) {
TreeNode cur = queue.poll();
max = Math.max(max, cur.val);
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
ans.add(max);
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(n),队列
116. 填充每个节点的下一个右侧节点指针 (Populating Next Right Pointers in Each Node)
树深度优先搜索广度优先搜索链表二叉树中等
题目
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
PLAINTEXT
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
示例 1:
输入:root = [1,2,3,4,5,6,7]
输出:[1,#,2,3,#,4,5,6,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的
输出:按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束。
示例 2:
输入:root = []
输出:[]
提示:
- 树中节点的数量在
[0, 212 - 1]范围内 -1000 <= node.val <= 1000- 进阶:你只能使用常量级额外空间。
- 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
思路简述
完美二叉树:逐层用已连好的 next 横穿。对每个结点:left.next = right,若还有隔壁父结点则 right.next = next.left。额外空间 O(1)。
参考代码
JAVA
class Solution {
public Node connect(Node root) {
if (root == null) {
return null;
}
// leftmost:当前层最左结点;完美二叉树有左孩子则必有下一层
Node leftmost = root;
while (leftmost.left != null) {
Node head = leftmost;
while (head != null) {
// 同一父结点下的左右孩子
head.left.next = head.right;
// 跨父结点:右孩子接到隔壁的左孩子
if (head.next != null) {
head.right.next = head.next.left;
}
head = head.next;
}
leftmost = leftmost.left; // 下到下一层最左
}
return root;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
117. 填充每个节点的下一个右侧节点指针 II (Populating Next Right Pointers in Each Node II)
树深度优先搜索广度优先搜索链表二叉树中等
题目
给定一个二叉树:
PLAINTEXT
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL 。
初始状态下,所有 next 指针都被设置为 NULL 。
示例 1:
输入:root = [1,2,3,4,5,null,7]
输出:[1,#,2,3,#,4,5,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化
输出:按层序遍历顺序(由 next 指针连接),'#' 表示每层的末尾。
示例 2:
输入:root = []
输出:[]
提示:
- 树中的节点数在范围
[0, 6000]内 -100 <= Node.val <= 100- 进阶:你只能使用常量级额外空间。
- 使用递归解题也符合要求,本题中递归程序的隐式栈空间不计入额外空间复杂度。
思路简述
非完美树:用当前层已连好的 next 横穿,同时用 哑结点 串起下一层孩子(有左挂左、有右挂右)。层结束后 curr = dummy.next 进入下一层。O(1) 额外空间。
参考代码
JAVA
class Solution {
public Node connect(Node root) {
Node curr = root; // 当前层最左结点;沿 next 当作链表遍历本层
while (curr != null) {
Node dummy = new Node(0); // 下一层虚头,方便统一接孩子
Node tail = dummy; // 下一层已接好的链尾
// 本层已由 next 串好,从左扫到右,按出现顺序挂上下一层的孩子
for (Node node = curr; node != null; node = node.next) {
if (node.left != null) {
tail.next = node.left;
tail = tail.next;
}
if (node.right != null) {
tail.next = node.right;
tail = tail.next;
}
}
curr = dummy.next; // 进入下一层(可能为 null,循环结束)
}
return root;
}
}
复杂度
- 时间:O(n)
- 空间:O(1) 额外(每层一个哨兵)
104. 二叉树的最大深度 (Maximum Depth of Binary Tree)
树深度优先搜索广度优先搜索二叉树简单
题目
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:3
示例 2:
输入:root = [1,null,2]
输出:2
提示:
- 树中节点的数量在
[0, 10^4]区间内。 -100 <= Node.val <= 100
思路简述
后序递归:左右子树深度取 max,再 +1(加上当前根)。空结点深度为 0。
参考代码
JAVA
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
}
复杂度
- 时间:O(n)
- 空间:O(h),递归栈
111. 二叉树的最小深度 (Minimum Depth of Binary Tree)
树深度优先搜索广度优先搜索二叉树简单
题目
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:
输入:root = [2,null,3,null,4,null,5,null,6]
输出:5
提示:
- 树中节点数的范围在
[0, 105]内 -1000 <= Node.val <= 1000
思路简述
最小深度是到最近叶子的结点个数。与最大深度不同:若只有一侧孩子,不能把空侧当成深度 0(否则会得到 1)。递归时只对非空子树求深度,再取 min 后 +1。
BFS 遇到第一个叶子即可返回,偏斜树时往往更快;渐近仍为 O(n)。
参考代码
JAVA
class Solution {
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
// 叶子:左右都空
if (root.left == null && root.right == null) {
return 1;
}
// 空孩子不参与 min(避免被当成深度 0)
int left = root.left != null ? minDepth(root.left) : Integer.MAX_VALUE;
int right = root.right != null ? minDepth(root.right) : Integer.MAX_VALUE;
return Math.min(left, right) + 1;
}
}
复杂度
- 时间:O(n)
- 空间:O(h),递归栈
226. 翻转二叉树 (Invert Binary Tree)
树深度优先搜索广度优先搜索二叉树简单
题目
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
示例 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
思路简述
递归翻转左右子树,再交换当前结点的 left / right 指针。
参考代码
JAVA
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
}
}
复杂度
- 时间:O(n)
- 空间:O(h),递归栈
101. 对称二叉树 (Symmetric Tree)
树深度优先搜索广度优先搜索二叉树简单
题目
给你一个二叉树的根节点 root , 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
提示:
- 树中节点数目在范围
[1, 1000]内 -100 <= Node.val <= 100- 进阶:你可以运用递归和迭代两种方法解决这个问题吗?
思路简述
对称 ⟺ 左右子树互为镜像:同时比较两棵子树,要求值相等,且 A.left 与 B.right、A.right 与 B.left 分别镜像。
参考代码
JAVA
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
return check(root.left, root.right);
}
// 判断两棵树是否互为镜像
private boolean check(TreeNode a, TreeNode b) {
if (a == null || b == null) {
return a == null && b == null; // 必须同时为空
}
if (a.val != b.val) {
return false;
}
return check(a.left, b.right) && check(a.right, b.left);
}
}
复杂度
- 时间:O(n)
- 空间:O(h),递归栈
222. 完全二叉树的节点个数 (Count Complete Tree Nodes)
位运算树二分查找二叉树中等
题目
给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。
完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(从第 0 层开始),则该层包含 1~ 2h 个节点。
示例 1:
输入:root = [1,2,3,4,5,6]
输出:6
示例 2:
输入:root = []
输出:0
示例 3:
输入:root = [1]
输出:1
提示:
- 树中节点的数目范围是
[0, 5 104] 0 <= Node.val <= 5 104- 题目数据保证输入的树是 完全二叉树
- 进阶:遍历树来统计节点是一种时间复杂度为
O(n)的简单解决方案。你可以设计一个更快的算法吗?
思路简述
完全二叉树:先求最左深度 h;上层满树共 2^h-1 个结点。对最后一层下标 [0, 2^h-1] 二分,按堆下标位运算探路判断结点是否存在,找到最右存在下标。总时间 O(log² n)。
参考代码
JAVA
class Solution {
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
// h:根到最左叶的边数;h==0 表示只有根
int h = 0;
TreeNode cur = root;
while (cur.left != null) {
h++;
cur = cur.left;
}
if (h == 0) {
return 1;
}
// 二分最后一层:找最右仍存在的下标 low(0-based)
int low = 0;
int high = (1 << h) - 1;
while (low < high) {
int mid = low + ((high - low + 1) >> 1); // 偏右中点,避免死循环
if (exists(root, h, mid)) {
low = mid;
} else {
high = mid - 1;
}
}
// 上层满树 2^h-1 + 最后一层 low+1 个结点
int ans = (1 << h) - 1 + low + 1;
return ans;
}
// 按完全二叉树堆下标:从高到低看 idx 各位,0 走左、1 走右
private boolean exists(TreeNode root, int h, int idx) {
int mask = 1 << (h - 1);
TreeNode node = root;
while (mask > 0 && node != null) {
node = (idx & mask) == 0 ? node.left : node.right;
mask >>= 1;
}
return node != null;
}
}
朴素 DFS/层序是 O(n)。也可比较左右最深高度:相等则左满用公式,否则递归左右,同为 O(log² n)。
复杂度
- 时间:O(log² n),二分 O(log n) 次,每次探路 O(log n)
- 空间:O(1)
110. 平衡二叉树 (Balanced Binary Tree)
树深度优先搜索二叉树简单
题目
给定一个二叉树,判断它是否是 平衡二叉树
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:true
示例 2:
输入:root = [1,2,2,3,3,null,null,4,4]
输出:false
示例 3:
输入:root = []
输出:true
提示:
- 树中的节点数在范围
[0, 5000]内 -10^4 <= Node.val <= 10^4
思路简述
自底向上一次 DFS:在求高度的同时判断平衡;若左右高度差 >1 或子树已不平衡,返回 -1 作为「非法高度」向上传递。每个结点只访问一次,O(n)。
先 getHeight 再递归 isBalanced 正确,但同一结点会被反复求高,最坏 O(n²)。
参考代码
JAVA
class Solution {
public boolean isBalanced(TreeNode root) {
return height(root) != -1;
}
// 返回子树高度;若不平衡返回 -1
private int height(TreeNode node) {
if (node == null) {
return 0;
}
int left = height(node.left);
if (left == -1) {
return -1; // 左子树已不平衡,剪枝
}
int right = height(node.right);
if (right == -1) {
return -1;
}
if (Math.abs(left - right) > 1) {
return -1;
}
return Math.max(left, right) + 1;
}
}
复杂度
- 时间:O(n)
- 空间:O(h)
257. 二叉树的所有路径 (Binary Tree Paths)
树深度优先搜索字符串回溯二叉树简单
题目
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]
示例 2:
输入:root = [1]
输出:["1"]
提示:
- 树中节点的数目在范围
[1, 100]内 -100 <= Node.val <= 100
思路简述
DFS 回溯:用 StringBuilder(或路径列表)记录根到当前结点的路径;到达叶子时把路径加入答案,返回时删掉当前结点。
参考代码
JAVA
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> ans = new ArrayList<>();
if (root == null) {
return ans;
}
StringBuilder path = new StringBuilder();
dfs(root, path, ans);
return ans;
}
private void dfs(TreeNode node, StringBuilder path, List<String> ans) {
int len = path.length(); // 记录回溯点
path.append(node.val);
// 叶子:形成一条完整路径
if (node.left == null && node.right == null) {
ans.add(path.toString());
} else {
path.append("->");
if (node.left != null) {
dfs(node.left, path, ans);
}
if (node.right != null) {
dfs(node.right, path, ans);
}
}
path.setLength(len); // 回溯,恢复进入本结点前的状态
}
}
复杂度
- 时间:O(n·h),路径拷贝与拼接(n 个结点,路径长 O(h))
- 空间:O(h),递归栈与路径缓冲
404. 左叶子之和 (Sum of Left Leaves)
树深度优先搜索广度优先搜索二叉树简单
题目
给定二叉树的根节点 root ,返回所有左叶子之和。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:24
解释:在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24
示例 2:
输入:root = [1]
输出:0
提示:
- 节点数在
[1, 1000]范围内 -1000 <= Node.val <= 1000
思路简述
DFS:若某结点的左孩子是叶子,累加其值;否则继续递归左右子树。注意根本身不是左叶子。
参考代码
JAVA
class Solution {
private int ans;
public int sumOfLeftLeaves(TreeNode root) {
ans = 0;
dfs(root);
return ans;
}
private void dfs(TreeNode node) {
if (node == null) {
return;
}
// 左孩子存在且为叶子 → 计入答案
if (node.left != null) {
if (node.left.left == null && node.left.right == null) {
ans += node.left.val;
} else {
dfs(node.left);
}
}
dfs(node.right);
}
}
复杂度
- 时间:O(n)
- 空间:O(h)
513. 找树左下角的值 (Find Bottom Left Tree Value)
树深度优先搜索广度优先搜索二叉树中等
题目
给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。
假设二叉树中至少有一个节点。
示例 1:
输入:root = [2,1,3]
输出:1
示例 2:
输入:[1,2,3,4,null,5,6,null,null,7]
输出:7
提示:
- 二叉树的节点个数的范围是
[1,104] -231 <= Node.val <= 231 - 1
思路简述
最底层最左结点。BFS:每层第一个出队结点更新答案,最后一层保留的即结果。DFS:先左后右,用深度刷新「当前最大深度对应的值」,额外空间 O(h)。
参考代码(解法一:DFS,空间更优)
JAVA
class Solution {
private int ans;
private int maxDepth = -1;
public int findBottomLeftValue(TreeNode root) {
dfs(root, 0);
return ans;
}
private void dfs(TreeNode node, int depth) {
if (node == null) {
return;
}
// 先左后右:同一深度第一次到达的是最左
if (depth > maxDepth) {
maxDepth = depth;
ans = node.val;
}
dfs(node.left, depth + 1);
dfs(node.right, depth + 1);
}
}
复杂度(DFS)
- 时间:O(n)
- 空间:O(h)
参考代码(解法二:BFS)
JAVA
class Solution {
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
int ans = root.val;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode cur = queue.poll();
if (i == 0) {
ans = cur.val; // 本层最左;最后一层留下的即答案
}
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
}
return ans;
}
}
复杂度(BFS)
- 时间:O(n)
- 空间:O(n),队列
112. 路径总和 (Path Sum)
树深度优先搜索广度优先搜索二叉树简单
题目
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。
示例 1:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。
示例 2:
输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径: (1 --> 2): 和为 3 (1 --> 3): 和为 4 不存在 sum = 5 的根节点到叶子节点的路径。
示例 3:
输入:root = [], targetSum = 0
输出:false
解释:由于树是空的,所以不存在根节点到叶子节点的路径。
提示:
- 树中节点的数目在范围
[0, 5000]内 -1000 <= Node.val <= 1000-1000 <= targetSum <= 1000
思路简述
DFS 从根往下扣减:到叶子时判断 remain == node.val;非叶子对左右递归,任一为真即可。空树无路径,返回 false。
参考代码
JAVA
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
boolean ans = dfs(root, targetSum);
return ans;
}
// remain:从当前结点出发还需要凑的路径和
private boolean dfs(TreeNode node, int remain) {
if (node == null) {
return false; // 空不是合法路径;也覆盖整树为空
}
if (node.left == null && node.right == null) {
return remain == node.val; // 必须到叶子才判定
}
return dfs(node.left, remain - node.val)
|| dfs(node.right, remain - node.val);
}
}
复杂度
- 时间:O(n)
- 空间:O(h),递归栈
105. 从前序与中序遍历序列构造二叉树 (Construct Binary Tree from Preorder and Inorder Traversal)
树数组哈希表分治二叉树中等
题目
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入:preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出:[3,9,20,null,null,15,7]
示例 2:
输入:preorder = [-1], inorder = [-1]
输出:[-1]
提示:
1 <= preorder.length <= 3000inorder.length == preorder.length-3000 <= preorder[i], inorder[i] <= 3000preorder和inorder均 无重复 元素inorder均出现在preorderpreorder保证 为二叉树的前序遍历序列inorder保证 为二叉树的中序遍历序列
思路简述
前序第一个是根;在中序中定位根,左边为左子树、右边为右子树。用哈希表存中序下标,递归划分区间。时间 O(n)。
不要用 static Map:多组测试会残留旧映射。每次 buildTree 新建或 clear。
参考代码
JAVA
class Solution {
private Map<Integer, Integer> inIndex; // 中序值 -> 下标
public TreeNode buildTree(int[] preorder, int[] inorder) {
inIndex = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inIndex.put(inorder[i], i);
}
return build(preorder, 0, preorder.length - 1, 0, inorder.length - 1);
}
// pre[preL..preR]、in[inL..inR] 对应同一棵子树
private TreeNode build(int[] preorder, int preL, int preR, int inL, int inR) {
if (preL > preR) {
return null;
}
TreeNode root = new TreeNode(preorder[preL]);
int index = inIndex.get(root.val); // 根在中序中的位置
int leftCnt = index - inL; // 左子树结点个数
root.left = build(preorder, preL + 1, preL + leftCnt, inL, index - 1);
root.right = build(preorder, preL + leftCnt + 1, preR, index + 1, inR);
return root;
}
}
复杂度
- 时间:O(n)
- 空间:O(n),哈希表与递归栈
106. 从中序与后序遍历序列构造二叉树 (Construct Binary Tree from Inorder and Postorder Traversal)
树数组哈希表分治二叉树中等
题目
给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。
示例 1:
输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出:[3,9,20,null,null,15,7]
示例 2:
输入:inorder = [-1], postorder = [-1]
输出:[-1]
提示:
1 <= inorder.length <= 3000postorder.length == inorder.length-3000 <= inorder[i], postorder[i] <= 3000inorder和postorder都由 不同 的值组成postorder中每一个值都在inorder中inorder保证是树的中序遍历postorder保证是树的后序遍历
思路简述
后序最后一个是根;在中序定位后划分左右子树,再对后序区间递归。哈希表存中序下标,O(n)。勿用 static Map。
参考代码
JAVA
class Solution {
private Map<Integer, Integer> inIndex; // 中序值 -> 下标
public TreeNode buildTree(int[] inorder, int[] postorder) {
inIndex = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inIndex.put(inorder[i], i);
}
return build(inorder, 0, inorder.length - 1, postorder, 0, postorder.length - 1);
}
// in[inL..inR]、post[postL..postR] 对应同一棵子树
private TreeNode build(int[] inorder, int inL, int inR, int[] postorder, int postL, int postR) {
if (postL > postR) {
return null;
}
TreeNode root = new TreeNode(postorder[postR]); // 后序末尾是根
int index = inIndex.get(root.val);
int leftCnt = index - inL; // 左子树结点个数
root.left = build(inorder, inL, index - 1, postorder, postL, postL + leftCnt - 1);
root.right = build(inorder, index + 1, inR, postorder, postL + leftCnt, postR - 1);
return root;
}
}
复杂度
- 时间:O(n)
- 空间:O(n)
654. 最大二叉树 (Maximum Binary Tree)
栈树数组分治二叉树单调栈中等
题目
给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:
创建一个根节点,其值为
nums中的最大值。递归地在最大值 左边 的 子数组前缀上 构建左子树。
递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。
示例 1:
输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
解释:递归调用如下所示: - [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。 - [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。 - 空数组,无子节点。 - [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。 - 空数组,无子节点。 - 只有一个元素,所以子节点是一个值为 1 的节点。 - [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。 - 只有一个元素,所以子节点是一个值为 0 的节点。 - 空数组,无子节点。
示例 2:
输入:nums = [3,2,1]
输出:[3,null,2,null,1]
提示:
1 <= nums.length <= 10000 <= nums[i] <= 1000nums中的所有整数 互不相同
思路简述
最大值作根,左右子数组递归建子树。区间内扫最大值是 O(n²);用堆找最大更慢。最优用单调递减栈:从左到右建结点,弹出所有比当前小的接到左孩子,再把当前挂到栈顶右孩子,最后栈底即全树根,O(n)。
参考代码
JAVA
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
Deque<TreeNode> stack = new ArrayDeque<>(); // 栈顶到栈底:值递减
for (int val : nums) {
TreeNode cur = new TreeNode(val);
// 弹出所有比 cur 小的结点;最后弹出的是 cur 左子树的根
while (!stack.isEmpty() && stack.peek().val < val) {
cur.left = stack.pop();
}
// 若仍有更大的前驱,cur 成为其右孩子
if (!stack.isEmpty()) {
stack.peek().right = cur;
}
stack.push(cur);
}
// 不断弹出后剩下的栈底是全局最大值,即 ans
TreeNode ans = null;
while (!stack.isEmpty()) {
ans = stack.pop();
}
return ans;
}
}
每次在 [L,R] 线性找 max 下标再递归左右;比 PriorityQueue 每层建堆更好。n≤1000 可通过,但非时间最优。
复杂度
- 时间:O(n),每个元素至多入栈出栈一次
- 空间:O(n),栈与结果树
617. 合并二叉树 (Merge Two Binary Trees)
树深度优先搜索广度优先搜索二叉树简单
题目
给你两棵二叉树: root1 和 root2 。
想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。
返回合并后的二叉树。
注意: 合并过程必须从两个树的根节点开始。
示例 1:
输入:root1 = [1,3,2,5], root2 = [2,1,3,null,4,null,7]
输出:[3,4,5,5,4,null,7]
示例 2:
输入:root1 = [1], root2 = [1,2]
输出:[2,2]
提示:
- 两棵树中的节点数目在范围
[0, 2000]内 -10^4 <= Node.val <= 10^4
思路简述
两棵树同步 DFS:重叠结点值相加;一侧为空则直接挂另一侧整棵子树,不必再新建。时间 O(min(n₁,n₂)) 量级(只遍历重叠部分)。
参考代码
JAVA
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null) {
return root2; // 左树空,整段右树可直接复用
}
if (root2 == null) {
return root1; // 右树空,整段左树可直接复用
}
// 两侧都非空:新建结点,值相加,再递归合并左右孩子
TreeNode ans = new TreeNode(root1.val + root2.val);
ans.left = mergeTrees(root1.left, root2.left);
ans.right = mergeTrees(root1.right, root2.right);
return ans;
}
}
若允许改 root1,可写成 root1.val += root2.val 后原地挂左右,少一次 new;一侧为空时仍应直接 return,勿把 null 当 0 再整树拷贝。
复杂度
- 时间:O(n),n 为两树重叠结点数(一侧为空即停止)
- 空间:O(h),递归栈深度 h 为合并路径最大高度
700. 二叉搜索树中的搜索 (Search in a Binary Search Tree)
树二叉搜索树二叉树简单
题目
给定二叉搜索树(BST)的根节点 root 和一个整数值 val。
你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null 。
示例 1:
输入:root = [4,2,7,1,3], val = 2
输出:[2,1,3]
示例 2:
输入:root = [4,2,7,1,3], val = 5
输出:[]
提示:
- 树中节点数在
[1, 5000]范围内 1 <= Node.val <= 10^7root是二叉搜索树1 <= val <= 10^7
思路简述
BST:val 小于当前走左,大于走右,相等即命中。迭代沿路下探,额外空间 O(1);递归写法等价,栈深 O(h)。
参考代码
JAVA
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
TreeNode ans = root;
while (ans != null && ans.val != val) {
// 小于当前值 → 答案只可能在左子树;否则在右子树
ans = val < ans.val ? ans.left : ans.right;
}
return ans; // 命中返回该结点子树根;走到 null 则不存在
}
}
if (root == null || root.val == val) return root; return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val); 时间同阶,额外空间 O(h)。
复杂度
- 时间:O(h),平衡约为 O(log n),最坏链状 O(n)
- 空间:O(1),迭代无递归栈
98. 验证二叉搜索树 (Validate Binary Search Tree)
树深度优先搜索二叉搜索树二叉树中等
题目
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 严格小于 当前节点的数。
节点的右子树只包含 严格大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
输入:root = [2,1,3]
输出:true
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
提示:
- 树中节点数目范围在
[1, 10^4]内 -231 <= Node.val <= 231 - 1
思路简述
BST 要求每个结点落在开区间 (min, max):左子树上界改为当前值,右子树下界改为当前值。用 long 作边界,避免结点取到 Integer.MIN/MAX 时无法区分。也可中序遍历检查严格递增。
参考代码
JAVA
class Solution {
public boolean isValidBST(TreeNode root) {
// 初始开区间 (-∞, +∞);用 long 包住 int 全范围
return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
// 判断以 root 为根的子树是否所有结点都满足 min < val < max
private boolean dfs(TreeNode root, long min, long max) {
if (root == null) {
return true;
}
if (root.val <= min || root.val >= max) {
return false; // 不在开区间内(BST 要求严格小于/大于)
}
// 左:(min, root.val);右:(root.val, max)
return dfs(root.left, min, root.val) && dfs(root.right, root.val, max);
}
}
中序遍历时维护 prev,若当前 val <= prev 则非法,额外空间同为 O(h)(Morris 可 O(1))。上下界法一次 DFS 即可,代码更短。
复杂度
- 时间:O(n),每个结点访问一次
- 空间:O(h),递归栈;最坏链状 O(n)
530. 二叉搜索树的最小绝对差 (Minimum Absolute Difference in BST)
树深度优先搜索广度优先搜索二叉搜索树二叉树简单
题目
给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。
差值是一个正数,其数值等于两值之差的绝对值。
示例 1:
输入:root = [4,2,6,1,3]
输出:1
示例 2:
输入:root = [1,0,48,null,null,12,49]
输出:1
提示:
- 树中节点的数目范围是
[2, 104] 0 <= Node.val <= 10^5- 注意:本题与 783 https://leetcode.cn/problems/minimum-distance-between-bst-nodes/ 相同
思路简述
BST 中序有序,最小差必出现在相邻中序结点之间。用 Morris 中序边走边与前驱比差,额外空间 O(1)。
参考代码
JAVA
class Solution {
public int getMinimumDifference(TreeNode root) {
int ans = Integer.MAX_VALUE;
int prevVal = 0; // 中序上一个已访问结点的值
boolean isFirst = true; // 尚无前驱时不比较
while (root != null) {
if (root.left != null) {
// 找中序前驱:左子树一路向右
TreeNode predecessor = root.left;
while (predecessor.right != null && predecessor.right != root) {
predecessor = predecessor.right;
}
if (predecessor.right == null) {
predecessor.right = root; // 建线索,先走左
root = root.left;
continue;
}
predecessor.right = null; // 第二次到达:拆线索
}
// 中序访问当前结点
if (!isFirst) {
ans = Math.min(ans, root.val - prevVal); // 有序,差非负
}
prevVal = root.val;
isFirst = false;
root = root.right;
}
return ans;
}
}
递归/栈中序维护 prev 同样 O(n) 时间,额外空间 O(h)。本题结点数 ≥ 2,ans 终会更新。
复杂度
- 时间:O(n),每个结点至多访问常数次
- 空间:O(1),Morris 不借递归栈(不计改指针临时线索)
501. 二叉搜索树中的众数 (Find Mode in Binary Search Tree)
树深度优先搜索二叉搜索树二叉树简单
题目
给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。
如果树中有不止一个众数,可以按 任意顺序 返回。
假定 BST 满足如下定义:
结点左子树中所含节点的值 小于等于 当前节点的值
结点右子树中所含节点的值 大于等于 当前节点的值
左子树和右子树都是二叉搜索树
示例 1:
输入:root = [1,null,2,2]
输出:[2]
示例 2:
输入:root = [0]
输出:[0]
提示:
- 树中节点的数目在范围
[1, 10^4]内 -10^5 <= Node.val <= 10^5- 进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)
思路简述
BST 中序有序,相同值连续出现。不用 HashMap:用 Morris 中序 扫两遍——第一遍求最大频次 maxCnt,第二遍统计有多少个众数并分配答案数组,第三遍写入(或第二遍用 ArrayList 收集)。答案数组本身不算「额外空间」。单遍无法既知全局最大频次又定长开数组。
参考代码
JAVA
class Solution {
public int[] findMode(TreeNode root) {
int maxCnt = 0;
int curCnt = 0;
Integer prev = null;
// 第一遍 Morris:求最大频次
TreeNode curr = root;
while (curr != null) {
if (curr.left != null) {
TreeNode pred = curr.left;
while (pred.right != null && pred.right != curr) {
pred = pred.right;
}
if (pred.right == null) {
pred.right = curr;
curr = curr.left;
continue;
}
pred.right = null;
}
// 中序访问
if (prev != null && curr.val == prev) {
curCnt++;
} else {
curCnt = 1; // 换值时从 1 开始,不要置 0
}
maxCnt = Math.max(maxCnt, curCnt);
prev = curr.val;
curr = curr.right;
}
// 第二遍:统计众数个数,再开定长数组
int modeNum = 0;
curCnt = 0;
prev = null;
curr = root;
while (curr != null) {
if (curr.left != null) {
TreeNode pred = curr.left;
while (pred.right != null && pred.right != curr) {
pred = pred.right;
}
if (pred.right == null) {
pred.right = curr;
curr = curr.left;
continue;
}
pred.right = null;
}
if (prev != null && curr.val == prev) {
curCnt++;
} else {
curCnt = 1;
}
if (curCnt == maxCnt) {
modeNum++; // 每个众数值在「达到 maxCnt 的那一次」计一次
}
prev = curr.val;
curr = curr.right;
}
int[] ans = new int[modeNum];
int idx = 0;
curCnt = 0;
prev = null;
curr = root;
// 第三遍:写入 ans
while (curr != null) {
if (curr.left != null) {
TreeNode pred = curr.left;
while (pred.right != null && pred.right != curr) {
pred = pred.right;
}
if (pred.right == null) {
pred.right = curr;
curr = curr.left;
continue;
}
pred.right = null;
}
if (prev != null && curr.val == prev) {
curCnt++;
} else {
curCnt = 1;
}
if (curCnt == maxCnt) {
ans[idx++] = curr.val;
}
prev = curr.val;
curr = curr.right;
}
return ans;
}
}
换值时
currCnt应置 1 不是 0;最后一组还要用max收尾(或像上文每次访问都更新)。单遍不知道全局
maxCnt,无法直接定长开数组;要两/三遍,或第二遍用List收集。调试用的
println/return null删掉。
复杂度
- 时间:O(n)
- 空间:O(1) 额外(Morris;不计答案数组)
236. 二叉树的最近公共祖先 (Lowest Common Ancestor of a Binary Tree)
树深度优先搜索二叉树中等
题目
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
提示:
- 树中节点数目在范围
[2, 105]内。 -109 <= Node.val <= 10^9- 所有
Node.val互不相同。 p != qp和q均存在于给定的二叉树中。
思路简述
后序 DFS:命中 p/q 或空则返回;左右都非空说明分居两侧,当前根即 LCA;只在一侧则向上传递该侧结果(含「祖先即自身」)。
参考代码
JAVA
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 空,或命中 p/q:题目保证二者都在树中,可直接返回
if (root == null || root == p || root == q) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 左右都找到:p、q 分居两侧,root 为最近公共祖先
if (left != null && right != null) {
return root;
}
// 只在一侧:向上传递(可能是 LCA,也可能是 p/q 本身)
TreeNode ans = left != null ? left : right;
return ans;
}
}
复杂度
- 时间:O(n),最坏遍历整棵树
- 空间:O(h),递归栈;最坏链状 O(n)
235. 二叉搜索树的最近公共祖先 (Lowest Common Ancestor of a Binary Search Tree)
树深度优先搜索二叉搜索树二叉树中等
题目
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
示例 1:
输入:root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出:6
解释:节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入:root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出:2
解释:节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
提示:
- 所有节点的值都是唯一的。
p、q为不同节点且均存在于给定的二叉搜索树中。
思路简述
BST:若当前值同时小于 p、q 则 LCA 在右;同时大于则在左;否则当前即为分叉点(或命中其一)。迭代下探,额外空间 O(1)。勿套用 236 全树后序。
参考代码
JAVA
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode ans = root;
while (ans != null) {
if (ans.val < p.val && ans.val < q.val) {
ans = ans.right; // p、q 都在右子树
} else if (ans.val > p.val && ans.val > q.val) {
ans = ans.left; // p、q 都在左子树
} else {
// 分居两侧,或 ans 等于 p/q 之一
return ans;
}
}
return null; // 题目保证存在,实际不会走到
}
}
同理三分支:都小走右、都大走左、否则 return root。时间同阶,额外空间 O(h)。
复杂度
- 时间:O(h),平衡约 O(log n),最坏链状 O(n)
- 空间:O(1),迭代无递归栈
701. 二叉搜索树中的插入操作 (Insert into a Binary Search Tree)
树二叉搜索树二叉树中等
题目
给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果 。
示例 1:
输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是:
示例 2:
输入:root = [40,20,60,10,30,50,70], val = 25
输出:[40,20,60,10,30,50,70,null,null,25]
示例 3:
输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5
输出:[4,2,7,1,3,5]
提示:
- 树中的节点数将在
[0, 10^4]的范围内。 -108 <= Node.val <= 10^8- 所有值
Node.val是 独一无二 的。 -108 <= val <= 10^8- 保证
val在原始BST中不存在。
思路简述
按 BST 规则下探:val 小于当前走左,大于走右,遇到空位挂上新结点。迭代额外空间 O(1);递归亦可,空树直接 return new TreeNode(val)。
参考代码
JAVA
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
TreeNode node = new TreeNode(val);
if (root == null) {
return node; // 空树:新结点即根
}
TreeNode cur = root;
while (true) {
if (val < cur.val) {
if (cur.left == null) {
cur.left = node; // 找到左空位插入
break;
}
cur = cur.left;
} else { // 题目保证 val 不与已有值重复
if (cur.right == null) {
cur.right = node;
break;
}
cur = cur.right;
}
}
return root; // 原根不变(非空时)
}
}
if (root == null) return new TreeNode(val); if (val < root.val) root.left = insertIntoBST(root.left, val); else root.right = insertIntoBST(root.right, val); return root; 不必在父层再判子结点是否为 null。
复杂度
- 时间:O(h)
- 空间:O(1),迭代
450. 删除二叉搜索树中的节点 (Delete Node in a BST)
树二叉搜索树二叉树中等
题目
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点;
如果找到了,删除它。
示例 1:
输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]
解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。 一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。 另一个正确答案是 [5,2,6,null,4,null,7]。
示例 2:
输入:root = [5,3,6,2,4,null,7], key = 0
输出:[5,3,6,2,4,null,7]
解释:二叉树不包含值为 0 的节点
示例 3:
输入:root = [], key = 0
输出:[]
提示:
- 节点数的范围
[0, 10^4]. -10^5 <= Node.val <= 10^5- 节点值唯一
root是合法的二叉搜索树-10^5 <= key <= 10^5- 进阶: 要求算法时间复杂度为 O(h),h 为树的高度。
思路简述
先按 BST 找到目标;删叶子/单孩子:父指针改挂唯一孩子。双孩子:取右子树最左(后继),把目标左子树挂到后继左侧,再用右子树整体顶替目标。哨兵 dummy 统一处理删根。迭代 O(1) 额外空间。
参考代码
JAVA
class Solution {
public TreeNode deleteNode(TreeNode root, int key) {
TreeNode dummy = new TreeNode(0); // 哨兵:dummy.right 始终指向当前树根
dummy.right = root;
TreeNode parent = dummy;
TreeNode cur = root;
// 定位待删结点,并记录其父结点
while (cur != null && cur.val != key) {
parent = cur;
cur = key < cur.val ? cur.left : cur.right;
}
if (cur == null) {
return dummy.right; // 未找到,原树不变
}
if (cur.left == null || cur.right == null) {
// 0 或 1 个孩子:用仅有的孩子(或 null)顶替 cur
TreeNode child = cur.left != null ? cur.left : cur.right;
if (parent.left == cur) {
parent.left = child;
} else {
parent.right = child;
}
} else {
// 双孩子:后继 = 右子树最左;左子树挂到后继左侧,右子树顶替 cur
TreeNode succ = cur.right;
while (succ.left != null) {
succ = succ.left;
}
succ.left = cur.left;
if (parent.left == cur) {
parent.left = cur.right;
} else {
parent.right = cur.right;
}
}
TreeNode ans = dummy.right;
return ans;
}
}
命中后:无左返右、无右返左;双孩子则把后继值拷到当前再 deleteNode(root.right, succ.val)。时间同 O(h),额外空间 O(h)。
复杂度
- 时间:O(h)
- 空间:O(1),迭代 + 哨兵
669. 修剪二叉搜索树 (Trim a Binary Search Tree)
树深度优先搜索二叉搜索树二叉树中等
题目
给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。
所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。
示例 1:
输入:root = [1,0,2], low = 1, high = 2
输出:[1,null,2]
示例 2:
输入:root = [3,0,4,null,2,null,null,1], low = 1, high = 3
输出:[3,2,null,1]
提示:
- 树中节点数在范围
[1, 10^4]内 0 <= Node.val <= 10^4- 树中每个节点的值都是 唯一 的
- 题目数据保证输入是一棵有效的二叉搜索树
0 <= low <= high <= 10^4
思路简述
利用 BST:root.val < low 则左子树全废,只保留修剪后的右子树;root.val > high 同理只留左。落在 [low, high] 内则递归修剪左右并挂回。勿套用 450 删结点(你写法里递归新建 dummy,parent.left == root 几乎永不成立)。
参考代码
JAVA
class Solution {
public TreeNode trimBST(TreeNode root, int low, int high) {
if (root == null) {
return null;
}
if (root.val < low) {
// 当前及左子树都 < low,答案只可能在右子树
return trimBST(root.right, low, high);
}
if (root.val > high) {
// 当前及右子树都 > high,答案只可能在左子树
return trimBST(root.left, low, high);
}
// 当前保留,递归修剪左右
root.left = trimBST(root.left, low, high);
root.right = trimBST(root.right, low, high);
return root;
}
}
复杂度
- 时间:O(n),最坏访问每个结点一次
- 空间:O(h),递归栈
108. 将有序数组转换为二叉搜索树 (Convert Sorted Array to Binary Search Tree)
树二叉搜索树数组分治二叉树简单
题目
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。
示例 1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例 2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
提示:
1 <= nums.length <= 10^4-10^4 <= nums[i] <= 10^4nums按 严格递增 顺序排列
思路简述
有序数组即 BST 中序。每次取区间中点作根,左右半段递归建子树,高度自然平衡。
参考代码
JAVA
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
TreeNode ans = build(nums, 0, nums.length - 1);
return ans;
}
// 用 nums[start..end] 建平衡 BST
private TreeNode build(int[] nums, int start, int end) {
if (start > end) {
return null;
}
int mid = start + (end - start) / 2; // 中点作根,左右规模尽量均衡
TreeNode root = new TreeNode(nums[mid]);
root.left = build(nums, start, mid - 1);
root.right = build(nums, mid + 1, end);
return root;
}
}
复杂度
- 时间:O(n),每个元素恰建一个结点
- 空间:O(log n),平衡树递归深度(不计结果树本身)
538. 把二叉搜索树转换为累加树 (Convert BST to Greater Tree)
树深度优先搜索二叉搜索树二叉树中等
题目
给出二叉 搜索 树的根节点 root,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),将其转换为一个更大的树,使得原始二叉搜索树中的每个节点值都变为原本值加上原本二叉搜索树中所有比该节点值大的节点值的总和。
提醒一下,二叉搜索树满足下列约束条件:
节点的左子树仅包含键 小于 节点键的节点。
节点的右子树仅包含键 大于 节点键的节点。
左右子树也必须是二叉搜索树。
注意:本题和 1038: https://leetcode.cn/problems/binary-search-tree-to-greater-sum-tree/ 相同
示例 1:
输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]
示例 2:
输入:root = [0,null,1]
输出:[1,null,1]
示例 3:
输入:root = [1,0,2]
输出:[3,3,2]
示例 4:
输入:root = [3,2,4,1]
输出:[7,9,4,10]
提示:
- 树中的节点数介于
0和104^ 之间。 - 每个节点的值介于
-10^4和104之间。 - 树中的所有值 互不相同 。
- 给定的树为二叉搜索树。
思路简述
累加树:每个结点变为「自身 + 所有更大值之和」。按 右 → 根 → 左 反中序,从大到小累加 sum 写回。用 Morris 借右子树最左空指针作线索,额外空间 O(1)。
参考代码
JAVA
class Solution {
public TreeNode convertBST(TreeNode root) {
TreeNode ans = root;
int sum = 0; // 已访问(更大)结点值之和
TreeNode curr = root;
while (curr != null) {
if (curr.right != null) {
// 后继:右子树一路向左
TreeNode prev = curr.right;
while (prev.left != null && prev.left != curr) {
prev = prev.left;
}
if (prev.left == null) {
prev.left = curr; // 建线索,先走右
curr = curr.right;
continue;
}
prev.left = null; // 第二次到达:拆线索
}
// 反中序访问:先累加再写回
sum += curr.val;
curr.val = sum;
curr = curr.left;
}
return ans;
}
}
递归反中序维护成员/外层 sum 同理,额外空间 O(h)。本题与 1038 相同。
复杂度
- 时间:O(n)
- 空间:O(1),Morris(不计改指针临时线索)
8. 回溯算法 (Backtracking)
核心思想: 组合 / 分割 / 子集 / 排列 / 棋盘与其它。
题目列表
组合问题
77. 组合 (Combinations) 中等 标签:数组, 回溯
216. 组合总和 III (Combination Sum III) 中等 标签:数组, 回溯
17. 电话号码的字母组合 (Letter Combinations of a Phone Number) 中等 标签:哈希表, 字符串, 回溯
39. 组合总和 (Combination Sum) 中等 标签:数组, 回溯
40. 组合总和 II (Combination Sum II) 中等 标签:数组, 回溯
分割问题
131. 分割回文串 (Palindrome Partitioning) 中等 标签:字符串, 动态规划, 回溯
93. 复原 IP 地址 (Restore IP Addresses) 中等 标签:字符串, 回溯
子集问题
78. 子集 (Subsets) 中等 标签:位运算, 数组, 回溯
90. 子集 II (Subsets II) 中等 标签:位运算, 数组, 回溯
排列问题
46. 全排列 (Permutations) 中等 标签:数组, 回溯
47. 全排列 II (Permutations II) 中等 标签:数组, 回溯
棋盘问题
51. N 皇后 (N-Queens) 困难 标签:数组, 回溯
37. 解数独 (Sudoku Solver) 困难 标签:数组, 哈希表, 回溯, 矩阵
其他
491. 递增子序列 (Non-decreasing Subsequences) 中等 标签:位运算, 数组, 哈希表, 回溯
332. 重新安排行程 (Reconstruct Itinerary) 困难 标签:深度优先搜索, 图, 欧拉回路
77. 组合 (Combinations)
回溯中等
题目
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:[ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
示例 2:
输入:n = 1, k = 1
输出:1
提示:
1 <= n <= 201 <= k <= n
思路简述
回溯选组合:下一层从 i+1 开始,保证升序、不重复。有 start 后不需要 used。剪枝:还要选 need 个数时,i 最大为 n - need + 1。
参考代码
JAVA
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> ans = new ArrayList<>();
dfs(n, k, 1, new ArrayList<>(), ans);
return ans;
}
// start:本层枚举起点;path:当前已选数字
private void dfs(int n, int k, int start, List<Integer> path, List<List<Integer>> ans) {
if (path.size() == k) {
ans.add(new ArrayList<>(path));
return;
}
// 剪枝:剩余位置不够凑满 k 个则停止
int need = k - path.size();
for (int i = start; i <= n - need + 1; i++) {
path.add(i);
dfs(n, k, i + 1, path, ans); // 下一层从 i+1 起,避免重复组合
path.remove(path.size() - 1);
}
}
}
排列才需要 used;组合靠 start = i+1 即可。你原写法正确但 used 冗余。
复杂度
- 时间:O(C(n,k)·k),每个组合拷贝一次
- 空间:O(k),递归深度(不计答案)
216. 组合总和 III (Combination Sum III)
数组回溯中等
题目
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入:k = 3, n = 7
输出:1,2,4
解释:1 + 2 + 4 = 7 没有其他符合的组合了。
示例 2:
输入:k = 3, n = 9
输出:[[1,2,6], [1,3,5], [2,3,4]]
解释:1 + 2 + 6 = 9 1 + 3 + 5 = 9 2 + 3 + 4 = 9 没有其他符合的组合了。
示例 3:
输入:k = 4, n = 1
输出:[]
解释:不存在有效的组合。 在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
提示:
2 <= k <= 91 <= n <= 60
思路简述
在 [1,9] 中选 k 个互异数使和为 n:下一层 i+1 保证升序。剪枝两处:剩余可选个数不够则停;当前数已大于 remain 则后面更大,直接 break。
参考代码
JAVA
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> ans = new ArrayList<>();
dfs(1, k, n, new ArrayList<>(), ans);
return ans;
}
// start:本层枚举起点;remain:还差多少凑到 n
private void dfs(int start, int k, int remain, List<Integer> path, List<List<Integer>> ans) {
if (path.size() == k) {
if (remain == 0) {
ans.add(new ArrayList<>(path));
}
return;
}
int need = k - path.size();
// i 最大:后面还要留 need-1 个比 i 大的数,且 i 不能超过 remain
for (int i = start; i <= 9 - need + 1; i++) {
if (i > remain) {
break; // 数字递增,后面更大
}
path.add(i);
dfs(i + 1, k, remain - i, path, ans);
path.remove(path.size() - 1);
}
}
}
你写的 9 - start + 1 < k - cnt 与循环上界 i <= 9 - need + 1 等价;用 remain 可再剪掉「和已超」。
复杂度
- 时间:O(C(9,k)·k),搜索空间至多从 1–9 选 k 个
- 空间:O(k),递归深度(不计答案)
17. 电话号码的字母组合 (Letter Combinations of a Phone Number)
哈希表字符串回溯中等
题目
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = "2"
输出:["a","b","c"]
提示:
1 <= digits.length <= 4digits[i]是范围['2', '9']的一个数字。
思路简述
每位数字对应一组字母,按位回溯:对当前数字枚举字母,递归下一位;StringBuilder 做选择/撤销。空串直接返回空列表。
参考代码
JAVA
class Solution {
// mapping[d]:数字 d 对应字母;0、1 占位
private static final String[] MAPPING = {
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
};
public List<String> letterCombinations(String digits) {
List<String> ans = new ArrayList<>();
if (digits == null || digits.isEmpty()) {
return ans;
}
dfs(digits, 0, new StringBuilder(), ans);
return ans;
}
// index:正在处理第几位;sb:当前字母路径
private void dfs(String digits, int index, StringBuilder sb, List<String> ans) {
if (index == digits.length()) {
ans.add(sb.toString());
return;
}
String letters = MAPPING[digits.charAt(index) - '0'];
for (int i = 0; i < letters.length(); i++) {
sb.append(letters.charAt(i));
dfs(digits, index + 1, sb, ans);
sb.deleteCharAt(sb.length() - 1);
}
}
}
复杂度
- 时间:O(4ⁿ·n),n 为位数,最坏每位 4 字母,每次拷贝长度 n
- 空间:O(n),递归深度与路径(不计答案)
39. 组合总和 (Combination Sum)
数组回溯中等
题目
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。 7 也是一个候选, 7 = 7 。 仅有这两种组合。
示例 2:
输入:candidates = [2,3,5], target = 8
输出:[[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入:candidates = [2], target = 1
输出:[]
提示:
1 <= candidates.length <= 302 <= candidates[i] <= 40candidates的所有元素 互不相同1 <= target <= 40
思路简述
回溯组合:下一层传 i(非 i+1)允许重复选当前数;start 保证下标非降,避免排列重复。先排序,若 candidates[i] > remain 可 break 剪枝。
参考代码
JAVA
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(candidates); // 便于按 remain 剪枝
dfs(candidates, 0, target, new ArrayList<>(), ans);
return ans;
}
// start:本层枚举起点;remain:还差多少凑到 target
private void dfs(int[] candidates, int start, int remain,
List<Integer> path, List<List<Integer>> ans) {
if (remain == 0) {
ans.add(new ArrayList<>(path));
return;
}
for (int i = start; i < candidates.length; i++) {
if (candidates[i] > remain) {
break; // 后面更大,无需再试
}
path.add(candidates[i]);
// 传 i:同一数字可再用;不回头选更小下标,防重复组合
dfs(candidates, i, remain - candidates[i], path, ans);
path.remove(path.size() - 1);
}
}
}
与你写法等价;用 remain 比维护 currSum 更短。元素互不相同,无需 40 题那种同层去重。
复杂度
- 时间:O(n^{T/m}) 量级(与解的数量相关;n 为候选数,m 为最小候选,T 为目标)
- 空间:O(T/m),递归深度(不计答案)
40. 组合总和 II (Combination Sum II)
数组回溯中等
题目
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入:candidates = [10,1,2,7,6,1,5], target = 8,
输出:[ [1,1,6], [1,2,5], [1,7], [2,6] ]
示例 2:
输入:candidates = [2,5,2,1,2], target = 5,
输出:[ [1,2,2], [5] ]
提示:
1 <= candidates.length <= 1001 <= candidates[i] <= 501 <= target <= 30
思路简述
与 39 的差别:每个下标只能用一次(下一层 i+1),且数组可有重复。先排序,同层跳过相同值:i > start && candidates[i] == candidates[i-1]。用 HashSet 去重会在重复很多时搜索树爆炸导致 TLE。
参考代码
JAVA
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(candidates);
dfs(candidates, 0, target, new ArrayList<>(), ans);
return ans;
}
private void dfs(int[] candidates, int start, int remain,
List<Integer> path, List<List<Integer>> ans) {
if (remain == 0) {
ans.add(new ArrayList<>(path));
return;
}
for (int i = start; i < candidates.length; i++) {
if (candidates[i] > remain) {
break; // 已排序,后面更大
}
// 同层去重:相同值只让第一个分支出去,避免 [1a,2] 与 [1b,2] 重复
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
path.add(candidates[i]);
dfs(candidates, i + 1, remain - candidates[i], path, ans); // i+1:每个数只用一次
path.remove(path.size() - 1);
}
}
}
复杂度
- 时间:O(2ⁿ·n) 最坏(剪枝后远小)
- 空间:O(n),递归深度(不计答案)
131. 分割回文串 (Palindrome Partitioning)
字符串动态规划回溯中等
题目
给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:"a"
提示:
1 <= s.length <= 16s仅由小写英文字母组成
思路简述
从 start 枚举切点 end,仅当 s[start..end] 为回文才继续切后缀。先 DP 预处理 isPal[i][j],判定 O(1)。
参考代码
JAVA
class Solution {
public List<List<String>> partition(String s) {
int n = s.length();
boolean[][] isPal = new boolean[n][n]; // isPal[i][j]:s[i..j] 是否回文
for (int i = n - 1; i >= 0; i--) {
for (int j = i; j < n; j++) {
isPal[i][j] = s.charAt(i) == s.charAt(j)
&& (j - i <= 1 || isPal[i + 1][j - 1]);
}
}
List<List<String>> ans = new ArrayList<>();
dfs(s, 0, isPal, new ArrayList<>(), ans);
return ans;
}
// start:s[0..start) 已切好
private void dfs(String s, int start, boolean[][] isPal,
List<String> path, List<List<String>> ans) {
if (start == s.length()) {
ans.add(new ArrayList<>(path));
return;
}
for (int end = start; end < s.length(); end++) {
if (!isPal[start][end]) {
continue; // 本段不是回文,不能切在这里
}
path.add(s.substring(start, end + 1));
dfs(s, end + 1, isPal, path, ans);
path.remove(path.size() - 1);
}
}
}
每次双指针判回文也对,n≤16 可通过;同一子串会被反复检查,预处理更优。
复杂度
- 时间:O(n·2ⁿ),方案数最多 2ⁿ⁻¹,每段 substring 为 O(n)
- 空间:O(n²) 的 DP 表 + O(n) 递归
93. 复原 IP 地址 (Restore IP Addresses)
字符串回溯中等
题目
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
- 例如:
"0.1.2.201"和"192.168.1.1"是 有效 IP 地址,但是"0.011.255.245"、"192.168.1.312"和"192.168@1.1"是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
示例 2:
输入:s = "0000"
输出:["0.0.0.0"]
示例 3:
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
提示:
1 <= s.length <= 20s仅由数字组成
思路简述
把串切成恰好 4 段,每段 1~3 位、值在 [0,255]、不能有前导零(单独一个 0 合法)。剩余长度不够或过长可剪枝。不要用 Integer.valueOf 判合法性:"01" 会变成 1。
参考代码
JAVA
class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> ans = new ArrayList<>();
dfs(s, 0, 0, new StringBuilder(), ans);
return ans;
}
// idx:下一段起始下标;seg:已切段数 0..4
private void dfs(String s, int idx, int seg, StringBuilder sb, List<String> ans) {
if (seg == 4) {
if (idx == s.length()) {
ans.add(sb.toString());
}
return;
}
int remain = s.length() - idx;
int need = 4 - seg;
if (remain < need || remain > 3 * need) {
return; // 剩下的字符填不满 / 塞不下剩余段
}
int maxLen = Math.min(3, remain);
for (int len = 1; len <= maxLen; len++) {
if (!valid(s, idx, idx + len - 1)) {
continue;
}
int old = sb.length();
if (seg > 0) {
sb.append('.');
}
sb.append(s, idx, idx + len);
dfs(s, idx + len, seg + 1, sb, ans);
sb.setLength(old); // 一次撤销点和数字
}
}
// s[l..r] 闭区间是否为合法 IP 段
private boolean valid(String s, int l, int r) {
int len = r - l + 1;
if (len > 1 && s.charAt(l) == '0') {
return false; // 前导零
}
int val = 0;
for (int i = l; i <= r; i++) {
val = val * 10 + (s.charAt(i) - '0');
}
return val <= 255;
}
}
Integer.valueOf("01") == 1,单靠 value > 255 拦不住前导零。你用「遇到 0 就 return、不再试更长段」碰巧能过,是因为循环从长度 1 开始;显式判 len>1 && s[l]=='0' 更稳。撤销用 setLength 比分别删数字和点更不容易错位。
复杂度
- 时间:O(1) 量级(n≤20 且恰好 4 段,搜索树很小)
- 空间:O(1) 递归深度最多 4(不计答案)
78. 子集 (Subsets)
位运算数组回溯中等
题目
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10-10 <= nums[i] <= 10nums中的所有元素 互不相同
思路简述
子集 = 每个位置选或不选。与组合不同:每个递归结点都收集 path(含空集),再从 index 起往后选。元素互异,下一层 i+1 即可。
参考代码
JAVA
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
dfs(nums, 0, new ArrayList<>(), ans);
return ans;
}
// index:本层从 nums[index] 起向后选
private void dfs(int[] nums, int index, List<Integer> path, List<List<Integer>> ans) {
ans.add(new ArrayList<>(path)); // 进入即收集:当前 path 已是一个子集
for (int i = index; i < nums.length; i++) {
path.add(nums[i]);
dfs(nums, i + 1, path, ans); // 后面只能选更大下标,避免 [1,2] 与 [2,1]
path.remove(path.size() - 1);
}
}
}
index == n 时 for 循环自然不执行,不必单独 return。也可用位运算:枚举 0..(1<<n)-1,第 i 位为 1 则放入 nums[i]。
复杂度
- 时间:O(n·2ⁿ),2ⁿ 个子集,每个拷贝最多 n 个元素
- 空间:O(n),递归深度(不计答案)
90. 子集 II (Subsets II)
位运算数组回溯中等
题目
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10-10 <= nums[i] <= 10
思路简述
在 78 上先排序,同层跳过相同值:i > index && nums[i] == nums[i-1]。必须是 i > index:同一层不重复选相同数字;不同层的两个 2 仍要能组成 [2,2]。
参考代码
JAVA
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums); // 相同值相邻
List<List<Integer>> ans = new ArrayList<>();
dfs(nums, 0, new ArrayList<>(), ans);
return ans;
}
private void dfs(int[] nums, int index, List<Integer> path, List<List<Integer>> ans) {
ans.add(new ArrayList<>(path));
for (int i = index; i < nums.length; i++) {
// 同层去重:本层已经用过 nums[i-1],跳过相同的 nums[i]
if (i > index && nums[i] == nums[i - 1]) {
continue;
}
path.add(nums[i]);
dfs(nums, i + 1, path, ans);
path.remove(path.size() - 1);
}
}
}
if (i>0 && nums[i]==nums[i-1]) path.add(...)没有大括号,且逻辑反了:相等才加入。应是相等就continue,再无条件add。i > 0会把「上一层选过的那个相同数」也跳掉,[1,2,2]里第二个2永远选不进,缺[2,2]、[1,2,2]。
复杂度
- 时间:O(n·2ⁿ)
- 空间:O(n),递归深度(不计答案)
46. 全排列 (Permutations)
数组回溯中等
题目
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:1
提示:
1 <= nums.length <= 6-10 <= nums[i] <= 10nums中的所有整数 互不相同
思路简述
排列要考虑顺序,每层从 0 扫到末尾,用 used 标记已选下标。与组合不同:组合靠 start 去重,排列靠 used。
参考代码
JAVA
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
boolean[] used = new boolean[nums.length]; // used[i]:nums[i] 是否已在路径中
dfs(nums, used, new ArrayList<>(), ans);
return ans;
}
private void dfs(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> ans) {
if (path.size() == nums.length) {
ans.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue; // 本排列里已用过该下标
}
used[i] = true;
path.add(nums[i]);
dfs(nums, used, path, ans);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
元素互不相同,无需 47 题那种「同层跳过相同值」。也可原地交换生成排列,省掉 used。
复杂度
- 时间:O(n·n!),n! 种排列,每种拷贝 n 个元素
- 空间:O(n),
used与递归深度(不计答案)
47. 全排列 II (Permutations II)
数组回溯排序中等
题目
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:[[1,1,2], [1,2,1], [2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8-10 <= nums[i] <= 10
思路简述
在 46 的 used 上,先排序,同层跳过相同值:i > 0 && nums[i] == nums[i-1] && !used[i-1]。含义是「前一个相同数字还没用,说明本层已经用它做过选择」。HashSet 只在叶子去重,重复分支仍会全部展开。
参考代码
JAVA
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums); // 相同值相邻,便于同层去重
boolean[] used = new boolean[nums.length];
dfs(nums, used, new ArrayList<>(), ans);
return ans;
}
private void dfs(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> ans) {
if (path.size() == nums.length) {
ans.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
// 同层去重:相同值只让「左边那个」先被选;used[i-1]==false 说明 i-1 在本层刚被撤销
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
used[i] = true;
path.add(nums[i]);
dfs(nums, used, path, ans);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
!used[i-1] 与 used[i-1] 都能过,前者剪的是树层(更常见)。不要用 HashSet 替代剪枝,n=8 全是相同数字时会多搜大量重复排列。
复杂度
- 时间:O(n·n!) 最坏(无重复时与 46 相同);有重复时剪枝更少
- 空间:O(n),
used与递归深度(不计答案)
51. N 皇后 (N-Queens)
数组回溯困难
题目
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:"Q"
提示:
1 <= n <= 9
思路简述
一行放一个皇后,列、主对角线、副对角线用布尔数组 O(1) 判冲突:列 j,副对角 row+col,主对角 row-col+n(平移避免负数)。收集答案后必须 return。
参考代码
JAVA
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> ans = new ArrayList<>();
char[][] board = new char[n][n];
for (int i = 0; i < n; i++) {
Arrays.fill(board[i], '.');
}
boolean[] col = new boolean[n];
boolean[] diag = new boolean[2 * n]; // 主对角:row - col + n
boolean[] anti = new boolean[2 * n - 1]; // 副对角:row + col
dfs(board, 0, n, col, diag, anti, ans);
return ans;
}
private void dfs(char[][] board, int row, int n, boolean[] col, boolean[] diag,
boolean[] anti, List<List<String>> ans) {
if (row == n) {
List<String> plan = new ArrayList<>(n);
for (char[] r : board) {
plan.add(new String(r));
}
ans.add(plan);
return; // 必须返回,否则 row==n 还会去改 board[n]
}
for (int j = 0; j < n; j++) {
int d = row - j + n;
int a = row + j;
if (col[j] || diag[d] || anti[a]) {
continue;
}
col[j] = diag[d] = anti[a] = true;
board[row][j] = 'Q';
dfs(board, row + 1, n, col, diag, anti, ans);
board[row][j] = '.';
col[j] = diag[d] = anti[a] = false;
}
}
}
你写法里三数组判定正确;idx == n 后漏了 return,会继续 board.get(n) 越界。zhu 长度用 2*n 才能覆盖 row-col+n 的最大值 2n-1。
复杂度
- 时间:O(n!) 量级(每行试列,冲突剪枝)
- 空间:O(n²) 棋盘 + O(n) 标记数组与递归
37. 解数独 (Sudoku Solver)
数组哈希表回溯矩阵困难
题目
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字
1-9在每一行只能出现一次。数字
1-9在每一列只能出现一次。数字
1-9在每一个以粗实线分隔的3x3宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.' 表示。
示例 1:
输入:board =
[["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释:
输入:的数独如上图所示,唯一有效的解决方案如下所示:
提示:
board.length == 9board[i].length == 9board[i][j]是一位数字或者'.'- 题目数据 保证 输入数独仅有一个解
思路简述
对每个 '.' 试填 1-9。行、列、宫用布尔数组 O(1) 判冲突:宫编号 (r/3)*3 + c/3。题目保证唯一解,填完即可 return true 结束。原地改 board,不必另造返回值。
参考代码
JAVA
class Solution {
public void solveSudoku(char[][] board) {
boolean[][] row = new boolean[9][10]; // row[i][d]:第 i 行是否已有数字 d
boolean[][] col = new boolean[9][10];
boolean[][] box = new boolean[9][10]; // 九宫:从上到下、从左到右编号 0..8
List<int[]> blanks = new ArrayList<>();
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.') {
blanks.add(new int[] { i, j });
continue;
}
int d = board[i][j] - '0';
row[i][d] = col[j][d] = box[i / 3 * 3 + j / 3][d] = true;
}
}
dfs(board, 0, blanks, row, col, box);
}
// k:正在填 blanks 的第 k 个空格;成功则 true(唯一解,找到即停)
private boolean dfs(char[][] board, int k, List<int[]> blanks,
boolean[][] row, boolean[][] col, boolean[][] box) {
if (k == blanks.size()) {
return true;
}
int r = blanks.get(k)[0];
int c = blanks.get(k)[1];
int b = r / 3 * 3 + c / 3;
for (int d = 1; d <= 9; d++) {
if (row[r][d] || col[c][d] || box[b][d]) {
continue;
}
board[r][c] = (char) ('0' + d);
row[r][d] = col[c][d] = box[b][d] = true;
if (dfs(board, k + 1, blanks, row, col, box)) {
return true; // 后面都填好了,不再撤销
}
board[r][c] = '.';
row[r][d] = col[c][d] = box[b][d] = false;
}
return false; // 1-9 都不行,回溯给上一格改数
}
}
每次填格时现扫 9 行 9 列 9 宫也能过,但重复扫描慢。空格预先收集后按列表下标递归,比双重循环找下一个 '.' 更干净。
复杂度
- 时间:最坏约 O(9^k),k 为空格数(≤81,实际远小)
- 空间:O(k) 空格列表 + O(1) 标记数组(9×10 固定)
491. 递增子序列 (Non-decreasing Subsequences)
位运算数组哈希表回溯中等
题目
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1]
输出:4,4
提示:
1 <= nums.length <= 15-100 <= nums[i] <= 100
思路简述
求长度至少为 2 的非递减子序列(相对顺序不能改),因此不能排序。回溯时只选 >= 路径末尾的数;同层相同值用 used 数组去重(值域 [-100,100])。与 90 不同:90 可排序后 i>index 跳过,本题位置顺序有意义。
参考代码
JAVA
class Solution {
public List<List<Integer>> findSubsequences(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
dfs(nums, 0, new ArrayList<>(), ans);
return ans;
}
private void dfs(int[] nums, int start, List<Integer> path, List<List<Integer>> ans) {
if (path.size() >= 2) {
ans.add(new ArrayList<>(path)); // 至少两个才合法,进入后先收集再继续加
}
boolean[] used = new boolean[201]; // 仅本层:used[x+100] 表示值 x 是否已选过
for (int i = start; i < nums.length; i++) {
if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)) {
continue; // 不能破坏非递减
}
if (used[nums[i] + 100]) {
continue; // 同层相同值只走一次,避免 [4,7a] 与 [4,7b] 重复
}
used[nums[i] + 100] = true;
path.add(nums[i]);
dfs(nums, i + 1, path, ans); // 子序列:后面只能用更大下标
path.remove(path.size() - 1);
}
}
}
used 在每一层新建,不回溯清除:它管的是「本层循环里这个值用过没」,不是路径上的占用。若用 HashSet<Integer> 同理。
复杂度
- 时间:O(n·2ⁿ),子集规模
- 空间:O(n),递归深度;每层
used为 O(1)(固定 201 格)
332. 重新安排行程 (Reconstruct Itinerary)
深度优先搜索图数组字符串排序欧拉回路堆(优先队列)困难
题目
给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
- 例如,行程
["JFK", "LGA"]与["JFK", "LGB"]相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
示例 1:
输入:tickets =
[["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
输出:["JFK","MUC","LHR","SFO","SJC"]
示例 2:
输入:tickets =
[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。
提示:
1 <= tickets.length <= 300tickets[i].length == 2fromi.length == 3toi.length == 3fromi和toi由大写英文字母组成fromi != toi
思路简述
每张票恰好用一次 = 有向图的欧拉路径(Hierholzer)。邻接用小根堆,保证先走字典序更小的边;走完一个点的所有出边后,再把该点插入结果头部(后序)。切勿对机票全排列回溯,n≤300 会爆。
参考代码
JAVA
class Solution {
public List<String> findItinerary(List<List<String>> tickets) {
Map<String, PriorityQueue<String>> graph = new HashMap<>();
for (List<String> t : tickets) {
graph.computeIfAbsent(t.get(0), k -> new PriorityQueue<>()).offer(t.get(1));
}
LinkedList<String> ans = new LinkedList<>();
dfs("JFK", graph, ans); // 题目规定从 JFK 出发
return ans;
}
private void dfs(String from, Map<String, PriorityQueue<String>> graph, LinkedList<String> ans) {
PriorityQueue<String> dests = graph.get(from);
// 先把当前机场所有未用机票走完(死胡同留到最后再入结果)
while (dests != null && !dests.isEmpty()) {
dfs(dests.poll(), graph, ans); // poll:用掉字典序最小的一张
}
ans.addFirst(from); // 后序插入:先走完的点排在后面
}
}
例:JFK→ATL、JFK→SFO。堆顶是 ATL,先走 ATL 整条支路,再走 SFO,最后得到字典序最小行程。poll 即删边,无需另开 used。
复杂度
- 时间:O(m log m),m 为票数,每条边入堆出堆一次
- 空间:O(m),邻接堆与递归栈
9. 贪心 (Greedy)
核心思想: 局部最优到全局最优:分配、区间、跳跃与股票等。
题目列表
455. 分发饼干 (Assign Cookies) 简单 标签:贪心, 数组, 双指针, 排序
376. 摆动序列 (Wiggle Subsequence) 中等 标签:贪心, 数组, 动态规划
53. 最大子数组和 (Maximum Subarray) 中等 标签:数组, 分治, 动态规划
122. 买卖股票的最佳时机 II (Best Time to Buy and Sell Stock II) 中等 标签:贪心, 数组, 动态规划
55. 跳跃游戏 (Jump Game) 中等 标签:贪心, 数组, 动态规划
45. 跳跃游戏 II (Jump Game II) 中等 标签:贪心, 数组, 动态规划
1005. K 次取反后最大化的数组和 (Maximize Sum Of An Array After K Negations) 简单 标签:贪心, 数组, 排序
134. 加油站 (Gas Station) 中等 标签:贪心, 数组
135. 分发糖果 (Candy) 困难 标签:贪心, 数组
860. 柠檬水找零 (Lemonade Change) 简单 标签:贪心, 数组
406. 根据身高重建队列 (Queue Reconstruction by Height) 中等 标签:贪心, 数组, 排序
452. 用最少数量的箭引爆气球 (Minimum Number of Arrows to Burst Balloons) 中等 标签:贪心, 数组, 排序
435. 无重叠区间 (Non-overlapping Intervals) 中等 标签:贪心, 数组, 动态规划, 排序
763. 划分字母区间 (Partition Labels) 中等 标签:贪心, 哈希表, 双指针, 字符串
56. 合并区间 (Merge Intervals) 中等 标签:数组, 排序
738. 单调递增的数字 (Monotone Increasing Digits) 中等 标签:贪心, 数学
968. 监控二叉树 (Binary Tree Cameras) 困难 标签:树, 深度优先搜索, 动态规划, 二叉树
455. 分发饼干 (Assign Cookies)
贪心数组双指针排序简单
题目
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是满足尽可能多的孩子,并输出这个最大数值。
示例 1:
输入:g = [1,2,3], s = [1,1]
输出:1
解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。 虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。 所以你应该
输出:1。
示例 2:
输入:g = [1,2], s = [1,2,3]
输出:2
解释:你有两个孩子和三块小饼干,2 个孩子的胃口值分别是 1,2。 你拥有的饼干数量和尺寸都足以让所有孩子满足。 所以你应该
输出:2。
提示:
1 <= g.length <= 3 1040 <= s.length <= 3 1041 <= g[i], s[j] <= 231 - 1- 注意:本题与 2410. 运动员和训练师的最大匹配数 题相同。
思路简述
排序后双指针贪心:胃口小的孩子优先,尽量用最小能喂饱他的饼干。s[j] >= g[i] 则配对成功,否则换更大的饼干。DP 可做但 O(nm) 空间,贪心更优。
参考代码
JAVA
class Solution {
public int findContentChildren(int[] g, int[] s) {
Arrays.sort(g); // 胃口从小到大
Arrays.sort(s); // 饼干从小到大
int ans = 0;
int j = 0; // 当前尝试的饼干下标
for (int i = 0; i < g.length; i++) {
// 找第一块能喂饱 g[i] 的饼干
while (j < s.length && s[j] < g[i]) {
j++;
}
if (j == s.length) {
break; // 饼干不够了
}
ans++;
j++; // 这块饼干已用,下一块
}
return ans;
}
}
也可写成 for (i, j) 双指针同步右移:能配对则 ans++ 且 i++,j 每次必 ++。本质相同:大饼干留给大胃口,小饼干先给小胃口。
复杂度
- 时间:O(n log n + m log m),排序;双指针 O(n + m)
- 空间:O(1),不计排序栈
376. 摆动序列 (Wiggle Subsequence)
贪心数组动态规划中等
题目
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如,
[1, 7, 4, 9, 2, 5]是一个 摆动序列 ,因为差值(6, -3, 5, -7, 3)是正负交替出现的。相反,
[1, 4, 7, 2, 5]和[1, 7, 4, 5, 5]不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
示例 1:
输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。
示例 2:
输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。 其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。
示例 3:
输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2
提示:
1 <= nums.length <= 10000 <= nums[i] <= 1000- 进阶:你能否用
O(n)时间复杂度完成此题?
思路简述
求最长摆动子序列,可删元素。单调上升/下降段只需保留两端,在峰或谷处转向才计入长度。扫一遍:差与上一次有效差符号相反(含首次非零差)则 ans++,并更新 prevDiff。全相等时答案为 1。
参考代码
JAVA
class Solution {
public int wiggleMaxLength(int[] nums) {
int n = nums.length;
if (n < 2) {
return n;
}
int prevDiff = 0; // 上一次计入摆动的差值;0 表示尚未形成方向
int ans = 1; // 至少保留一个数
for (int i = 1; i < n; i++) {
int diff = nums[i] - nums[i - 1];
// 出现新的峰/谷:与上一段方向相反;prevDiff==0 时第一次非零差也算
if ((diff > 0 && prevDiff <= 0) || (diff < 0 && prevDiff >= 0)) {
ans++;
prevDiff = diff; // 平坡(diff==0)不更新,继续等真正转向
}
}
return ans;
}
}
题目是子序列,不是连续子数组;
[1,17,5,10,13,15,...]中间可删掉单调段上的点。两个相等元素的最长摆动子序列长度是 1,不是 0。
遇到非峰谷就
cnt=0会把前面已经摆动的长度丢掉。
复杂度
- 时间:O(n)
- 空间:O(1)
53. 最大子数组和 (Maximum Subarray)
数组分治动态规划中等
题目
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4- 进阶:如果你已经实现复杂度为
O(n)的解法,尝试使用更为精妙的 分治法 求解。
思路简述
Kadane:维护以当前位置结尾的最大连续和 preSum。若前缀和已为负,对后续无贡献,直接从当前数重新开始;每步用 preSum 更新全局最大值 ans。
参考代码
JAVA
class Solution {
public int maxSubArray(int[] nums) {
int ans = Integer.MIN_VALUE; // 全局最大子数组和
int preSum = 0; // 以当前下标结尾的最大连续和
for (int num : nums) {
// 前缀已为负:丢弃,从当前数重新开一段
preSum = preSum < 0 ? num : preSum + num;
ans = Math.max(ans, preSum);
}
return ans;
}
}
也可写成 preSum = Math.max(num, preSum + num),与「preSum < 0 则重置」等价。分治法也能 O(n),常数更大,面试可提。
复杂度
- 时间:O(n)
- 空间:O(1)
122. 买卖股票的最佳时机 II (Best Time to Buy and Sell Stock II)
贪心数组动态规划中等
题目
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。然而,你可以在 同一天 多次买卖该股票,但要确保你持有的股票不超过一股。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 最大总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 最大总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。
提示:
1 <= prices.length <= 3 1040 <= prices[i] <= 10^4
思路简述
可多次买卖、同时最多持一股。最大利润等于把所有上涨段都吃掉:相邻两天若涨就加上差值,等价于在每个谷买、峰卖。
参考代码
JAVA
class Solution {
public int maxProfit(int[] prices) {
int ans = 0;
for (int i = 1; i < prices.length; i++) {
// 上涨就卖:等价于把每一段正收益都收入囊中
if (prices[i] > prices[i - 1]) {
ans += prices[i] - prices[i - 1];
}
}
return ans;
}
}
维护 minPrice,价格更低则更新;否则 ans += price - minPrice 并令 minPrice = price。与累加相邻正差等价,都是 O(n)/O(1)。
复杂度
- 时间:O(n)
- 空间:O(1)
55. 跳跃游戏 (Jump Game)
贪心数组动态规划中等
题目
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
提示:
1 <= nums.length <= 10^40 <= nums[i] <= 10^5
思路简述
维护当前能到达的最远下标 maxRight。遍历每个可达位置 i,用 i + nums[i] 更新最远;一旦盖住终点返回 true;若走到 i == maxRight 仍扩不出去(卡在 0),返回 false。
参考代码
JAVA
class Solution {
public boolean canJump(int[] nums) {
int maxRight = 0; // 当前能跳到的最远下标
for (int i = 0; i < nums.length; i++) {
maxRight = Math.max(maxRight, i + nums[i]); // 在 i 处起跳能到的最远
if (maxRight >= nums.length - 1) {
return true; // 已覆盖终点
}
if (i >= maxRight) {
return false; // 最远就卡在 i(通常 nums[i]==0),无法再前进
}
}
return true;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
45. 跳跃游戏 II (Jump Game II)
贪心数组动态规划中等
题目
给定一个长度为 n 的 0 索引整数数组 nums。初始位置在下标 0。
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在索引 i 处,你可以跳转到任意 (i + j) 处:
0 <= j <= nums[i]且i + j < n
返回到达 n - 1 的最小跳跃次数。测试用例保证可以到达 n - 1。
示例 1:
输入:nums = [2,3,1,1,4]
输出:2
解释:跳到最后一个位置的最小跳跃数是 2。 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
示例 2:
输入:nums = [2,3,0,1,4]
输出:2
提示:
1 <= nums.length <= 10^40 <= nums[i] <= 1000- 题目保证可以到达
n - 1
思路简述
在「当前这一跳」覆盖的区间里,记录再跳一次能到的最远 maxReach;走到当前跳的右边界 end 时,次数 +1,并把 end 更新为 maxReach。只扫到 n-2,避免在终点多算一跳。
参考代码
JAVA
class Solution {
public int jump(int[] nums) {
int ans = 0; // 最少跳跃次数
int maxReach = 0; // 当前跳覆盖范围内,再起跳能到的最远下标
int end = 0; // 当前这一跳的右边界
int n = nums.length;
// 不访问最后一个下标:若已能覆盖终点,不必从终点再起跳
for (int i = 0; i < n - 1; i++) {
maxReach = Math.max(maxReach, i + nums[i]);
if (i == end) {
// 本跳区间扫完,必须再跳一次
ans++;
end = maxReach;
if (end >= n - 1) {
return ans; // 新边界已盖过终点
}
}
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
1005. K 次取反后最大化的数组和 (Maximize Sum Of An Array After K Negations)
贪心数组排序简单
题目
给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:
- 选择某个下标
i并将nums[i]替换为-nums[i]。
重复这个过程恰好 k 次。可以多次选择同一个下标 i 。
以这种方式修改数组后,返回数组 可能的最大和 。
示例 1:
输入:nums = [4,2,3], k = 1
输出:5
解释:选择下标 1 ,nums 变为 [4,-2,3] 。
示例 2:
输入:nums = [3,-1,0,2], k = 3
输出:6
解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。
示例 3:
输入:nums = [2,-3,-1,5,-4], k = 2
输出:13
解释:选择下标 (1, 4) ,nums 变为 [2,3,-1,5,4] 。
提示:
1 <= nums.length <= 10^4-100 <= nums[i] <= 1001 <= k <= 10^4
思路简述
排序后优先把绝对值大的负数翻成正;若 k 仍有剩余:偶次再翻同一数抵消,奇次会少掉当前最小正数的两倍(ans - 2 * min)。
参考代码
JAVA
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
Arrays.sort(nums); // 负数在前,从小到大翻最亏的负数
int ans = 0;
int min = Integer.MAX_VALUE; // 翻完后数组中的最小值(用于剩余奇数次)
for (int num : nums) {
if (num < 0 && k > 0) {
num = -num; // 把负数变成正
k--;
}
ans += num;
min = Math.min(min, num);
}
// 负数不够翻:剩余 k 次;偶次等价于不改,奇次再翻一次最小值
if (k > 0 && k % 2 == 1) {
ans -= 2 * min;
}
return ans;
}
}
复杂度
- 时间:O(n log n),排序
- 空间:O(1),不计排序栈
134. 加油站 (Gas Station)
贪心数组中等
题目
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
示例 1:
输入:gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出:3
解释:从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 因此,3 可为起始索引。
示例 2:
输入:gas = [2,3,4], cost = [3,4,3]
输出:-1
解释:你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 因此,无论怎样,你都不可能绕环路行驶一周。
提示:
n == gas.length == cost.length1 <= n <= 10^50 <= gas[i], cost[i] <= 10^4- 输入保证答案唯一。
思路简述
令 gain[i] = gas[i] - cost[i]。若总和 < 0 则无解。环上从「前缀和最低点」的下一位出发,能保证途中油量始终非负(唯一解时即答案)。
参考代码
JAVA
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int sum = 0; // 前缀和:从 0 走到 i 的净油量累计
int min = 0; // 前缀和最小值
int ans = 0; // 最低点下一位,即候选起点
for (int i = 0; i < gas.length; i++) {
sum += gas[i] - cost[i];
if (sum < min) {
min = sum;
ans = (i + 1) % gas.length; // 最低点之后重新出发
}
}
return sum < 0 ? -1 : ans; // 总油不够则无解
}
}
再维护 tank:tank += gain,一旦 tank < 0 则起点改为 i+1 且 tank = 0;最后看总和是否非负。与「最低前缀和下一位」等价。
复杂度
- 时间:O(n)
- 空间:O(1)
135. 分发糖果 (Candy)
贪心数组困难
题目n 个孩子站成一排。
给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
每个孩子 至少 分配到
1个糖果。相邻两个孩子中,评分 更高 的那个会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少 糖果数目。
示例 1:
输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。
示例 2:
输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
提示:
1 <= n == ratings.length <= 5 1040 <= ratings[i] <= 5 104
思路简述
每人至少 1 颗。先从左到右:评分更高则比左边多 1;再从右到左:评分更高则取 max(当前, 右边+1),同时满足两侧约束。
参考代码
JAVA
class Solution {
public int candy(int[] ratings) {
int n = ratings.length;
int[] candies = new int[n];
Arrays.fill(candies, 1); // 每人至少 1 颗
// 从左到右:只保证比左边评分高时糖果更多
for (int i = 1; i < n; i++) {
if (ratings[i] > ratings[i - 1]) {
candies[i] = candies[i - 1] + 1;
}
}
int ans = candies[n - 1];
// 从右到左:补齐比右边评分高、但糖果还不够的情况
for (int i = n - 2; i >= 0; i--) {
if (ratings[i] > ratings[i + 1]) {
candies[i] = Math.max(candies[i], candies[i + 1] + 1);
}
ans += candies[i];
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(n),糖果数组
860. 柠檬水找零 (Lemonade Change)
贪心数组简单
题目
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
示例 1:
输入:bills = [5,5,5,10,20]
输出:true
解释:前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 由于所有客户都得到了正确的找零,所以我们
输出:true。
示例 2:
输入:bills = [5,5,10,10,20]
输出:false
解释:前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。 由于不是每位顾客都得到了正确的找零,所以答案是 false。
提示:
1 <= bills.length <= 10^5bills[i]不是5就是10或是20
思路简述
只统计 5 元、10 元张数。收 5 直接存;收 10 必须找一张 5;收 20 优先找「10+5」,否则找三张 5(留着 5 更灵活)。
参考代码
JAVA
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0; // 手头 5 元张数
int ten = 0; // 手头 10 元张数
for (int bill : bills) {
if (bill == 5) {
five++;
} else if (bill == 10) {
if (five == 0) {
return false; // 找不出 5
}
five--;
ten++;
} else { // 20:找 15
if (ten > 0 && five > 0) {
ten--;
five--; // 优先 10+5,多留 5 给后面
} else if (five >= 3) {
five -= 3;
} else {
return false;
}
}
}
return true;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
406. 根据身高重建队列 (Queue Reconstruction by Height)
树状数组线段树数组排序中等
题目
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。
请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
示例 1:
输入:people =
[[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 因此[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]是重新构造后的队列。
示例 2:
输入:people =
[[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
提示:
1 <= people.length <= 20000 <= hi <= 10^60 <= ki < people.length- 题目数据确保队列可以被重建
思路简述
先按身高从高到矮排,身高相同则 k 升序。高的人先入队:后面插入的矮人对已在队中的人「前面 ≥ 自己身高的人数」无影响。每个人插入到下标 k 的位置即可。
参考代码
JAVA
class Solution {
public int[][] reconstructQueue(int[][] people) {
// 身高降序;同身高按 k 升序(k 小的更靠前)
Arrays.sort(people, (a, b) -> a[0] != b[0] ? b[0] - a[0] : a[1] - b[1]);
List<int[]> queue = new LinkedList<>(); // 中间插入方便
for (int[] person : people) {
// person[1] 即 k:前面应有 k 个更高或等高的人;此时队中全是更高/等高,插到下标 k
queue.add(person[1], person);
}
return queue.toArray(new int[people.length][]);
}
}
复杂度
- 时间:O(n²),每次
List.add(index)最坏 O(n) - 空间:O(n),结果列表
452. 用最少数量的箭引爆气球 (Minimum Number of Arrows to Burst Balloons)
贪心数组排序中等
题目
有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。
示例 1:
输入:points =
[[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破: -在x = 6处射出箭,击破气球[2,8]和[1,6]。 -在x = 11处发射箭,击破气球[10,16]和[7,12]。
示例 2:
输入:points =
[[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。
示例 3:
输入:points =
[[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破: - 在x = 2处发射箭,击破气球[1,2]和[2,3]。 - 在x = 4处射出箭,击破气球[3,4]和[4,5]。
提示:
1 <= points.length <= 10^5points[i].length == 2-231 <= xstart < xend <= 231 - 1
思路简述
按气球右端点升序;每支箭射在当前区间右端点,能炸掉所有左端点 ≤ 该点的后续气球。遇左端点大于上支箭位置时再射一支。pre 用 long,避免端点为 Integer.MIN_VALUE 时初始化比较出错。
参考代码
JAVA
class Solution {
public int findMinArrowShots(int[][] points) {
Arrays.sort(points, Comparator.comparingInt(p -> p[1])); // 右端点升序
int ans = 0;
long pre = Long.MIN_VALUE; // 上一支箭的位置;用 long 防 MIN_VALUE 边界
for (int[] p : points) {
if (p[0] > pre) {
// 当前气球左端已超过上支箭,必须再射
ans++;
pre = p[1]; // 射在右端点,尽量多覆盖后面气球
}
// 否则 p[0] <= pre <= p[1],已被上支箭炸掉
}
return ans;
}
}
与 435 无重叠区间同型:按结束时间贪心。本题重叠(含端点相接)可共用一箭,故用 p[0] > pre 而非 >=。
复杂度
- 时间:O(n log n)
- 空间:O(1),不计排序栈
435. 无重叠区间 (Non-overlapping Intervals)
贪心数组动态规划排序中等
题目
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
注意 只在一点上接触的区间是 不重叠的。例如 [1, 2] 和 [2, 3] 是不重叠的。
示例 1:
输入:intervals =
[[1,2],[2,3],[3,4],[1,3]]
输出:1
解释:移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入:intervals = [ [1,2], [1,2], [1,2] ]
输出:2
解释:你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入:intervals = [ [1,2], [2,3] ]
输出:0
解释:你不需要移除任何区间,因为它们已经是无重叠的了。
提示:
1 <= intervals.length <= 10^5intervals[i].length == 2-5 104 <= starti < endi <= 5 104
思路简述
等价于「最多保留多少个不重叠区间」,答案 = 总数 − 保留数。按结束时间升序,能保留就保留(start >= prevEnd,端点相接算不重叠);否则删掉当前区间。
参考代码
JAVA
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1])); // 按右端点升序
int ans = 0; // 删除个数
int prevEnd = Integer.MIN_VALUE; // 上一个保留区间的右端点
for (int[] interval : intervals) {
if (interval[0] >= prevEnd) {
// 不重叠(含端点相接),保留
prevEnd = interval[1];
} else {
ans++; // 与上一段重叠,删掉结束更晚的(当前)
}
}
return ans;
}
}
452 端点相接可共用一箭,用 start > pre;本题相接不算重叠,用 start >= prevEnd。
复杂度
- 时间:O(n log n)
- 空间:O(1),不计排序栈
763. 划分字母区间 (Partition Labels)
贪心哈希表双指针字符串中等
题目
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 "ababcc" 能够被分为 ["abab", "cc"],但类似 ["aba", "bcc"] 或 ["ab", "ab", "cc"] 的划分是非法的。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
PLAINTEXT
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = "eccbbbbdec"
输出:[10]
提示:
1 <= s.length <= 500s仅由小写英文字母组成
思路简述
先记录每个字母最后出现下标。从左扫:当前片段右端取「片段内字母最右出现」的最大值;当 i 走到该右端即可切开,后面不会再出现片段内字母。
参考代码
JAVA
class Solution {
public List<Integer> partitionLabels(String s) {
int[] lastIndex = new int[26]; // 字母最后一次出现的下标
for (int i = 0; i < s.length(); i++) {
lastIndex[s.charAt(i) - 'a'] = i;
}
List<Integer> ans = new ArrayList<>();
int maxReach = 0; // 当前片段必须延伸到的最右下标
int left = 0; // 当前片段左端
for (int i = 0; i < s.length(); i++) {
maxReach = Math.max(maxReach, lastIndex[s.charAt(i) - 'a']);
if (i == maxReach) {
// 走到本片段右端:中间出现过的字母都不会再出现在后面
ans.add(i - left + 1);
left = i + 1;
}
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(1),固定 26 格(不计答案)
56. 合并区间 (Merge Intervals)
数组排序中等
题目
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals =
[[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals =
[[1,4],[4,5]]
输出:1,5
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
示例 3:
输入:intervals =
[[4,7],[1,4]]
输出:1,7
解释:区间 [1,4] 和 [4,7] 可被视为重叠区间。
提示:
1 <= intervals.length <= 10^4intervals[i].length == 20 <= starti <= endi <= 10^4
思路简述
按左端点排序后扫一遍:与答案末区间重叠(含端点相接)则扩展右端;否则追加新区间。
参考代码
JAVA
class Solution {
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); // 左端升序
List<int[]> ans = new ArrayList<>();
ans.add(intervals[0]);
for (int i = 1; i < intervals.length; i++) {
int[] last = ans.get(ans.size() - 1);
if (last[1] >= intervals[i][0]) {
// 重叠或相接:合并右端
last[1] = Math.max(last[1], intervals[i][1]);
} else {
ans.add(intervals[i]);
}
}
return ans.toArray(new int[ans.size()][]);
}
}
复杂度
- 时间:O(n log n)
- 空间:O(n),结果列表(不计排序栈)
738. 单调递增的数字 (Monotone Increasing Digits)
贪心数学中等
题目
当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。
示例 1:
输入:n = 10
输出:9
示例 2:
输入:n = 1234
输出:1234
示例 3:
输入:n = 332
输出:299
提示:
0 <= n <= 10^9
思路简述
从右往左扫:若 s[i] < s[i-1],则 s[i-1]--,并记下从 i 起后面都要变成 9(取尽可能大)。减一可能继续向左传导(如 332)。最后把标记位置及之后全部置 9。
参考代码
JAVA
class Solution {
public int monotoneIncreasingDigits(int n) {
char[] s = String.valueOf(n).toCharArray();
int nineFrom = s.length; // 从这个下标开始全部填 9
for (int i = s.length - 1; i > 0; i--) {
if (s[i] < s[i - 1]) {
s[i - 1]--; // 前一位减一,后面用 9 补满
nineFrom = i; // 不断左移:最终填 9 的起点是最左一次违规
}
}
for (int i = nineFrom; i < s.length; i++) {
s[i] = '9';
}
return Integer.parseInt(new String(s));
}
}
一边减一边把当前位改成 9,对 332 会变成 229(期望 299)。应先把减一传导完,再统一从最左违规位起填 9。
复杂度
- 时间:O(d),d 为位数(≤10)
- 空间:O(d)
968. 监控二叉树 (Binary Tree Cameras)
树深度优先搜索动态规划二叉树困难
题目
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:
输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。
示例 2:
输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
提示:
- 给定树的节点数的范围是
[1, 1000]。 - 每个节点的值都是 0。
思路简述
后序贪心:叶子不放摄像头,尽量放在叶子的父结点。每个结点返回三态——0 未覆盖、1 有摄像头、2 已被覆盖。子结点有未覆盖则本结点必须放摄像头;两子都已覆盖则本结点暂不放;有子带摄像头则本结点被覆盖。根若仍未覆盖再补一台。
参考代码
JAVA
class Solution {
private int ans;
public int minCameraCover(TreeNode root) {
ans = 0;
// 根仍未覆盖时,必须在根上再放一台
if (dfs(root) == 0) {
ans++;
}
return ans;
}
/**
* @return 0 未覆盖;1 本结点有摄像头;2 已被覆盖(无摄像头)
*/
private int dfs(TreeNode node) {
if (node == null) {
return 2; // 空结点视为「已覆盖」,不影响父结点决策
}
int left = dfs(node.left);
int right = dfs(node.right);
// 左右孩子都已被覆盖:本结点暂不放,留给父结点覆盖
if (left == 2 && right == 2) {
return 0;
}
// 有孩子未覆盖:本结点必须放摄像头
if (left == 0 || right == 0) {
ans++;
return 1;
}
// 其余:至少一个孩子有摄像头,本结点已被覆盖
// (left/right 为 1 或 2,且不是「双 2」——双 2 已在上面返回)
return 2;
}
}
隔层计数 / 减叶子数不是最优,也过不了多种树形。摄像头覆盖父、己、子三层,必须根据子树状态自底向上决策。
复杂度
- 时间:O(n)
- 空间:O(h),递归栈

































































