LeetCode Hot 100 题解笔记(Java)· 下
本文是 Hot 100 笔记下篇(图论到技巧)。上篇为哈希表到二叉树。
接上篇。本篇覆盖图论、回溯、二分、栈堆、贪心、动态规划与技巧。
目录
9. 图论 (Graph)
核心思想: 网格图多用 DFS/BFS;依赖关系用拓扑排序;前缀匹配用 Trie。先想清「节点 / 边」是什么。
题目列表
200. 岛屿数量 (Number of Islands) ⭐ ✅ 2026-08-13 标签:深度优先搜索, 广度优先搜索, 并查集, 数组, 矩阵
994. 腐烂的橘子 (Rotting Oranges) 🌟 ✅ 2026-08-13 标签:广度优先搜索, 数组, 矩阵
207. 课程表 (Course Schedule) ⭐ 🌟 ✅ 2026-08-13 标签:深度优先搜索, 广度优先搜索, 图, 拓扑排序
208. 实现 Trie (前缀树) (Implement Trie (Prefix Tree)) 🌟 ✅ 2026-08-13 标签:设计, 字典树, 哈希表, 字符串
200. 岛屿数量 (Number of Islands)
深度优先搜索广度优先搜索并查集数组矩阵中等
题目
给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请计算其中岛屿的数量。岛屿总是被水包围,且只能由水平或竖直方向相邻的陆地连接形成。可假设网格四条边均被水包围。
示例 1:
输入:grid =
[ ['1','1','1','1','0'], ['1','1','0','1','0'], ['1','1','0','0','0'], ['0','0','0','0','0'] ]
输出:1
示例 2:
输入:grid =
[ ['1','1','0','0','0'], ['1','1','0','0','0'], ['0','0','1','0','0'], ['0','0','0','1','1'] ]
输出:3
提示:
m == grid.length,n == grid[i].length1 <= m, n <= 300grid[i][j]为'0'或'1'
思路简述
遍历网格,每遇到一块未访问的陆地 '1',岛屿数 +1,再 DFS/BFS 把该连通块全部 淹没为 '0',避免重复计数。四方向扩散即可。
参考代码
JAVA
class Solution {
public int numIslands(char[][] grid) {
int rows = grid.length;
int cols = grid[0].length;
// ans:岛屿数量
int ans = 0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') {
// 新岛屿起点:计数后淹没整片连通陆地
ans++;
dfs(grid, i, j, rows, cols);
}
}
}
return ans;
}
// 从 (i, j) 出发,把上下左右相连的陆地全部改为 '0'
private void dfs(char[][] grid, int i, int j, int rows, int cols) {
if (i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] == '0') {
return;
}
grid[i][j] = '0'; // 标记已访问(原地修改)
dfs(grid, i - 1, j, rows, cols);
dfs(grid, i + 1, j, rows, cols);
dfs(grid, i, j - 1, rows, cols);
dfs(grid, i, j + 1, rows, cols);
}
}
复杂度
- 时间:O(m · n),每个格子至多进入 DFS 一次
- 空间:O(m · n),最坏整图为陆地时递归栈深度
994. 腐烂的橘子 (Rotting Oranges)
广度优先搜索数组矩阵中等
题目
在 m x n 网格 grid 中,每个单元格取值:0 空、1 新鲜橘子、2 腐烂橘子。每分钟,腐烂橘子四周相邻的新鲜橘子都会变腐烂。返回直到没有新鲜橘子为止所需的最小分钟数;若不可能则返回 -1。
示例 1:
输入:grid =
[[2,1,1],[1,1,0],[0,1,1]]
输出:4
示例 2:
输入:grid =
[[2,1,1],[0,1,1],[1,0,1]]
输出:-1
解释:左下角新鲜橘子永远不会腐烂。
示例 3:
输入:grid =
0,2
输出:0
解释:开始时已无新鲜橘子。
提示:
m == grid.length,n == grid[i].length1 <= m, n <= 10grid[i][j]仅为0、1或2
思路简述
多源 BFS:先把所有腐烂橘子入队,并统计新鲜数。每分钟处理完当前队列(本轮腐烂源),四向感染新鲜橘子并入队。BFS 层数即分钟数。若最终仍有新鲜橘子则返回 -1;一开始无新鲜则返回 0。
参考代码
JAVA
class Solution {
public int orangesRotting(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
// freshCount:剩余新鲜橘子数量
int freshCount = 0;
// queue:当前已腐烂、可向外扩散的格子
Queue<int[]> queue = new ArrayDeque<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
freshCount++;
} else if (grid[i][j] == 2) {
queue.offer(new int[]{i, j});
}
}
}
if (freshCount == 0) {
return 0;
}
// directions:上下左右四向
int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
int minutes = 0;
// 还有新鲜橘子且仍能扩散时继续按「分钟」分层 BFS
while (!queue.isEmpty() && freshCount > 0) {
minutes++;
int size = queue.size(); // 本分钟的腐烂源数量
for (int i = 0; i < size; i++) {
int[] cur = queue.poll();
for (int[] dir : directions) {
int nr = cur[0] + dir[0];
int nc = cur[1] + dir[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == 1) {
grid[nr][nc] = 2; // 感染
freshCount--;
queue.offer(new int[]{nr, nc});
}
}
}
}
// ans:全部腐烂所需分钟;若仍有新鲜则不可能
int ans = (freshCount == 0) ? minutes : -1;
return ans;
}
}
复杂度
- 时间:O(m · n),每个格子至多入队一次
- 空间:O(m · n),队列最坏装下整图
207. 课程表 (Course Schedule)
深度优先搜索广度优先搜索图拓扑排序中等
题目
你这个学期必须选修 numCourses 门课程,编号 0 到 numCourses - 1。先修关系由 prerequisites 给出,其中 prerequisites[i] = [ai, bi] 表示学 ai 前必须先学 bi。判断是否可能完成所有课程:可以返回 true,否则 false。
示例 1:
输入:numCourses = 2, prerequisites =
1,0
输出:true
解释:学课程 1 前需完成 0,可行。
示例 2:
输入:numCourses = 2, prerequisites =
[[1,0],[0,1]]
输出:false
解释:互相依赖形成环,不可能。
提示:
1 <= numCourses <= 20000 <= prerequisites.length <= 5000prerequisites[i].length == 20 <= ai, bi < numCourses- 先修对互不相同
思路简述
建模为有向图:bi → ai(先修指向后续)。能否修完等价于图中 无环。拓扑排序(Kahn):入度为 0 的课入队,学完后减少后继入度,新的 0 入度继续入队;若最终修完门数等于 numCourses 则无环。
参考代码
JAVA
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// adj:邻接表,adj[u] 为以 u 为先修的后继课程列表
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
adj.add(new ArrayList<>());
}
// indegree[c]:课程 c 尚需满足的先修数量
int[] indegree = new int[numCourses];
for (int[] pre : prerequisites) {
int course = pre[0];
int preCourse = pre[1];
indegree[course]++;
adj.get(preCourse).add(course); // preCourse → course
}
// 入度为 0:无先修,可立即学习
Queue<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < numCourses; i++) {
if (indegree[i] == 0) {
queue.offer(i);
}
}
int finished = 0; // 已修完的课程数
while (!queue.isEmpty()) {
int course = queue.poll();
finished++;
for (int next : adj.get(course)) {
indegree[next]--;
if (indegree[next] == 0) {
queue.offer(next);
}
}
}
// ans:能否修完全部课程(拓扑序列长度是否为 n)
boolean ans = (finished == numCourses);
return ans;
}
}
复杂度
- 时间:O(n + e),
n为课程数,e为先修边数 - 空间:O(n + e),邻接表与队列
208. 实现 Trie (前缀树) (Implement Trie (Prefix Tree))
设计字典树哈希表字符串中等
题目
Trie(前缀树)用于高效存储与检索字符串键。请实现:
Trie():初始化void insert(String word):插入wordboolean search(String word):若已插入完整单词word则返回trueboolean startsWith(String prefix):若已插入单词中存在前缀prefix则返回true
示例:
输入:
["Trie", "insert", "search", "search", "startsWith", "insert", "search"][[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出:[null, null, true, false, true, null, true]
解释:
Trie trie = new Trie(); trie.insert("apple"); trie.search("apple"); // true trie.search("app"); // false(尚未作为完整单词插入) trie.startsWith("app"); // true trie.insert("app"); trie.search("app"); // true
提示:
1 <= word.length, prefix.length <= 2000word、prefix仅含小写英文字母insert/search/startsWith总调用次数不超过3 * 10^4
思路简述
每个结点用长度为 26 的孩子数组表示 'a'..'z'。插入时沿字符下行,缺失则新建;终点打上 isEnd 标记完整单词。search 需走到末尾且 isEnd;startsWith 只需路径存在。
参考代码
JAVA
class Trie {
private class TrieNode {
boolean isEnd; // 是否有单词在此结束
TrieNode[] children; // children[c-'a']:下一字符对应子结点
TrieNode() {
isEnd = false;
children = new TrieNode[26];
}
}
private final TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode curr = root;
for (char c : word.toCharArray()) {
int idx = c - 'a';
if (curr.children[idx] == null) {
curr.children[idx] = new TrieNode();
}
curr = curr.children[idx];
}
curr.isEnd = true; // 标记完整单词终点
}
public boolean search(String word) {
TrieNode node = findNode(word);
// 路径存在且该结点是某单词结尾
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) {
// 前缀路径存在即可,不必 isEnd
return findNode(prefix) != null;
}
// 沿 prefix/word 下行;中途缺边返回 null,否则返回终点结点
private TrieNode findNode(String s) {
TrieNode curr = root;
for (char c : s.toCharArray()) {
int idx = c - 'a';
if (curr.children[idx] == null) {
return null;
}
curr = curr.children[idx];
}
return curr;
}
}
复杂度
- 时间:
insert/search/startsWith均为 O(L),L为字符串长度 - 空间:O(Σ L),与已插入字符结点总数同阶(最坏每字符一结点)
10. 回溯 (Backtracking)
核心思想: 在决策树上做 DFS:做选择 → 递归 → 撤销选择。注意剪枝与去重,模板一旦熟悉可套全排列 / 组合 / 子集。
题目列表
46. 全排列 (Permutations) ⭐ ✅ 2026-08-13 标签:数组, 回溯
78. 子集 (Subsets) ⭐ ✅ 2026-08-14 标签:位运算, 数组, 回溯
17. 电话号码的字母组合 (Letter Combinations of a Phone Number) ✅ 2026-08-14 标签:哈希表, 字符串, 回溯
39. 组合总和 (Combination Sum) ✅ 2026-08-14 标签:数组, 回溯
22. 括号生成 (Generate Parentheses) ✅ 2026-08-14 标签:字符串, 动态规划, 回溯
79. 单词搜索 (Word Search) ✅ 2026-08-14 标签:深度优先搜索, 数组, 字符串, 回溯, 矩阵
131. 分割回文串 (Palindrome Partitioning) ✅ 2026-08-14 标签:字符串, 动态规划, 回溯
51. N 皇后 (N-Queens) ⭐ ✅ 2026-08-14 标签:数组, 回溯
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中整数互不相同
思路简述
回溯:维护当前排列 path 与 used 标记。每一层从未使用的数字中选一个加入路径,递归到下一层;path 长度等于 n 时收集结果(需拷贝)。返回前 撤销选择(弹出并重置 used)。
参考代码
JAVA
class Solution {
public List<List<Integer>> permute(int[] nums) {
// ans:所有全排列
List<List<Integer>> ans = new ArrayList<>();
// used[i]:nums[i] 是否已在当前 path 中
boolean[] used = new boolean[nums.length];
backtrack(nums, used, new ArrayList<>(), ans);
return ans;
}
private void backtrack(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> ans) {
if (path.size() == nums.length) {
ans.add(new ArrayList<>(path)); // 必须拷贝,path 会被后续修改
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
// 做选择
used[i] = true;
path.add(nums[i]);
backtrack(nums, used, path, ans);
// 撤销选择
path.remove(path.size() - 1);
used[i] = false;
}
}
}
复杂度
- 时间:O(n · n!),共 n! 个排列,每个排列拷贝 O(n)
- 空间:O(n),递归深度与
path/used(不计答案)
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中元素互不相同
思路简述
回溯枚举:每一层从下标 start 起向后选数,保证相对顺序、避免重复子集。进入递归时先把当前 path 拷贝进答案(含空集),再尝试加入后续元素并撤销。用 start 控制 只能选更靠后的下标,与全排列的 used 不同。
参考代码
JAVA
class Solution {
public List<List<Integer>> subsets(int[] nums) {
// ans:所有子集(含空集)
List<List<Integer>> ans = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), ans);
return ans;
}
// start:本层可选起点;path:当前已选子集
private void backtrack(int[] nums, int start, List<Integer> path, List<List<Integer>> ans) {
ans.add(new ArrayList<>(path)); // 每个状态都是一个合法子集
if (start == nums.length) {
return;
}
for (int i = start; i < nums.length; i++) {
path.add(nums[i]); // 选择 nums[i]
backtrack(nums, i + 1, path, ans); // 下一层只能从 i+1 继续
path.remove(path.size() - 1); // 撤销
}
}
}
复杂度
- 时间:O(n · 2n),共 2n 个子集,每个拷贝至多 O(n)
- 空间:O(n),递归深度与
path(不计答案)
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']中的数字
思路简述
回溯:按位处理 digits,第 index 位枚举该数字对应的每个字母,追加后递归下一位;到末尾收集字符串并撤销。本质是 多叉树 DFS,每层分支为当前数字的字母表。
参考代码
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; // 空输入无组合
}
backtrack(digits, 0, new StringBuilder(), ans);
return ans;
}
// index:正在处理 digits 的第几位;sb:当前已选字母路径
private void backtrack(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 (char ch : letters.toCharArray()) {
sb.append(ch); // 做选择
backtrack(digits, index + 1, sb, ans);
sb.deleteCharAt(sb.length() - 1); // 撤销
}
}
}
复杂度
- 时间:O(4^n · n),
n为数字个数,最坏每位 4 个字母,每个结果拷贝 O(n) - 空间:O(n),递归深度与
StringBuilder(不计答案)
39. 组合总和 (Combination Sum)
数组回溯中等
题目
给你无重复元素的整数数组 candidates 与目标整数 target,找出所有使数字和为 target 的不同组合。同一数字可无限制重复选取;至少有一个数字选取次数不同即视为不同组合。可按任意顺序返回。保证合法组合数少于 150。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:2+2+3=7(2 可多次),以及单独的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
思路简述
回溯从下标 start 起选数,保证组合内非降下标顺序,避免 [2,3] 与 [3,2] 重复。因数字可复用,递归时传入 i 而非 i+1;currSum == target 收集,currSum > target 剪枝。
参考代码
JAVA
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
// ans:所有和为 target 的组合
List<List<Integer>> ans = new ArrayList<>();
// start=0, currSum=0:从数组开头开始选,当前路径和为 0
backtrack(candidates, 0, 0, target, new ArrayList<>(), ans);
return ans;
}
/**
* @param start 本层可选起点下标;只选 [start..] 保证组合按原数组下标非降,去重
* @param currSum 当前 path 中各数之和
* @param path 正在构造的组合
*/
private void backtrack(int[] candidates, int start, int currSum, int target,
List<Integer> path, List<List<Integer>> ans) {
if (currSum == target) {
ans.add(new ArrayList<>(path)); // 拷贝:path 后续还会被修改
return;
}
// 和已超目标,或没有剩余候选可选
if (currSum > target || start == candidates.length) {
return;
}
for (int i = start; i < candidates.length; i++) {
path.add(candidates[i]);
// 传入 i(不是 i+1):同一数字可再次被选;传入 start=i 仍禁止回到更小下标,防排列重复
backtrack(candidates, i, currSum + candidates[i], target, path, ans);
path.remove(path.size() - 1); // 去掉 candidates[i],尝试本层下一个候选
}
}
}
复杂度
- 时间:指数级,与合法组合数及搜索树规模相关(
target与最小值决定深度上界) - 空间:O(target / min(candidates)),递归深度上界(不计答案)
22. 括号生成 (Generate Parentheses)
字符串动态规划回溯中等
题目
数字 n 代表生成括号的对数,请生成所有可能且有效的括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
思路简述
回溯构造长度为 2n 的串:随时可在「左括号未用尽」时放 (;仅当 已放左括号多于右括号 时才能放 ),这样任意前缀都不会出现右括号过多,保证合法。左右都用满 n 个时收集答案。
参考代码
JAVA
class Solution {
public List<String> generateParenthesis(int n) {
// ans:所有合法括号串
List<String> ans = new ArrayList<>();
// left / right:当前路径中已使用的左、右括号个数;初始均为 0
backtrack(n, 0, 0, new StringBuilder(), ans);
return ans;
}
/**
* @param n 目标括号对数
* @param left 路径中已放入的 '(' 数量
* @param right 路径中已放入的 ')' 数量
* @param sb 当前正在构造的括号序列
* @param ans 收集完整合法串
*/
private void backtrack(int n, int left, int right,
StringBuilder sb, List<String> ans) {
// 左右都用满 n 个:得到长度 2n 的合法串
if (left == n && right == n) {
ans.add(sb.toString());
return;
}
// 还能放左括号:只要 left < n,多一个 '(' 不会破坏「右不超过左」的不变式
if (left < n) {
sb.append('(');
backtrack(n, left + 1, right, sb, ans);
sb.setLength(sb.length() - 1); // 去掉刚加的 '(',恢复供兄弟分支使用
}
// 能放右括号的充要条件:当前串中仍有未匹配的 '(',即 left > right
// 若 right == left,再放 ')' 会出现前缀不合法(如 "())...")
if (left > right) {
sb.append(')');
backtrack(n, left, right + 1, sb, ans);
sb.setLength(sb.length() - 1);
}
}
}
复杂度
- 时间:O(4^n / √n) 量级(第 n 个卡特兰数个合法串,每个串构造 O(n))
- 空间:O(n),递归深度与
StringBuilder(不计答案)
79. 单词搜索 (Word Search)
深度优先搜索数组字符串回溯矩阵中等
题目
给定 m x n 字符网格 board 与字符串 word。若 word 存在于网格中返回 true,否则 false。单词须按字母顺序由相邻(水平或垂直)单元格构成,同一单元格字母不可重复使用。
示例 1:
输入:board =
[['A','B','C','E'],['S','F','C','S'],['A','D','E','E']],word = "ABCCED"
输出:true
示例 2:
输入:board =
[['A','B','C','E'],['S','F','C','S'],['A','D','E','E']],word = "SEE"
输出:true
示例 3:
输入:board =
[['A','B','C','E'],['S','F','C','S'],['A','D','E','E']],word = "ABCB"
输出:false
提示:
m == board.length,n == board[i].length1 <= m, n <= 61 <= word.length <= 15board与word仅由大小写英文字母组成
进阶:能否用搜索剪枝在更大棋盘上更快?
思路简述
枚举每个格子作为起点,DFS 匹配 word 的第 index 个字符:越界、字符不符或已访问则失败;匹配则原地标 '#' 防重入,四向继续;回溯恢复字符。index == word.length() 表示整词匹配成功。
可先统计 board / word 字母频次,若词中某字母多于棋盘则直接 false;也可从词中更稀有的一端开始搜,减少无效分支。
参考代码
JAVA
class Solution {
public boolean exist(char[][] board, String word) {
if (board == null || board.length == 0) {
return false;
}
int rows = board.length;
int cols = board[0].length;
// 每个格子都可能是单词起点
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (dfs(board, i, j, 0, word)) {
return true;
}
}
}
return false;
}
/**
* 从格子 (i,j) 出发,尝试匹配 word 从下标 index 开始的后缀
* @return 能否完整匹配完 word[index..]
*/
private boolean dfs(char[][] board, int i, int j, int index, String word) {
// 已匹配完所有字符(上一步刚吃掉最后一个字母后 index 增至 length)
if (index == word.length()) {
return true;
}
int rows = board.length;
int cols = board[0].length;
// 越界,或当前格不是目标字符(含已访问标记 '#',也不等于 word[index])
if (i < 0 || j < 0 || i >= rows || j >= cols
|| board[i][j] != word.charAt(index)) {
return false;
}
// 占用当前格:先保存原字符,再改成哨兵,避免同一路径重复走入
char saved = board[i][j];
board[i][j] = '#';
// 四向任一方向能匹配剩余后缀即可;短路或,找到即停
boolean found = dfs(board, i + 1, j, index + 1, word)
|| dfs(board, i - 1, j, index + 1, word)
|| dfs(board, i, j + 1, index + 1, word)
|| dfs(board, i, j - 1, index + 1, word);
board[i][j] = saved; // 恢复棋盘,供其他起点/分支使用
return found;
}
}
复杂度
- 时间:O(m · n · 3^L),
L = word.length;每格出发,前进时约 3 个新方向(不含来路) - 空间:O(L),递归深度至多词长(原地标记,无额外 visited 矩阵)
131. 分割回文串 (Palindrome Partitioning)
字符串动态规划回溯中等
题目
给你一个字符串 s,请将 s 分割成若干子串,使每个子串都是回文串。返回所有可能的分割方案。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a"
输出:"a"
提示:
1 <= s.length <= 16s仅由小写英文字母组成
思路简述
回溯切分:从下标 start 起枚举这一段的结束位置 end(左闭右开),仅当 s[start..end) 是回文 才选入路径,再从 end 继续切。切到串尾时收集当前方案。用区间双指针判回文,避免反复 substring 造串再检查。
参考代码
JAVA
class Solution {
public List<List<String>> partition(String s) {
// ans:所有「整串切成回文子串」的方案
List<List<String>> ans = new ArrayList<>();
backtrack(s, 0, new ArrayList<>(), ans);
return ans;
}
/**
* @param start 当前待切分后缀的起始下标;表示 s[0..start) 已切好
* @param path 已切出的回文子串序列
*/
private void backtrack(String s, int start, List<String> path, List<List<String>> ans) {
// 起点顶到串尾:前面每一段都是回文,得到一种完整分割
if (start == s.length()) {
ans.add(new ArrayList<>(path));
return;
}
// end:本段右开边界,从 start+1 切到 s.length()(至少切出 1 个字符)
for (int end = start + 1; end <= s.length(); end++) {
// 只有 s[start..end) 本身是回文,才能作为合法的一段
if (!isPalindrome(s, start, end - 1)) {
continue;
}
path.add(s.substring(start, end));
// 下一段必须从 end 开始,保证覆盖整个字符串且不重叠
backtrack(s, end, path, ans);
path.remove(path.size() - 1); // 丢掉本段,尝试更长的 end
}
}
// 判断 s[left..right](闭区间)是否回文
private boolean isPalindrome(String s, int left, int right) {
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
}
复杂度
- 时间:O(n · 2n),约 2{n-1} 种切法,每次判回文与拷贝至多 O(n)
- 空间:O(n),递归深度与当前路径(不计答案)
51. N 皇后 (N-Queens)
数组回溯困难
题目
按照国际象棋规则,皇后可攻击同一行、同一列或同一斜线上的棋子。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
思路简述
按行放置:第 row 行枚举列下标 colIndex。用三个布尔数组 O(1) 判冲突——列、主对角线 ↘(row - colIndex 相同)、副对角线 ↙(row + colIndex 相同)。不冲突则放 Q、标记、递归下一行;回溯撤销。因 一行只放一个,无需再管行冲突。
参考代码
JAVA
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> ans = new ArrayList<>();
// board:可变棋盘,每行一个 StringBuilder,便于 setCharAt
List<StringBuilder> board = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
StringBuilder row = new StringBuilder();
for (int j = 0; j < n; j++) {
row.append('.');
}
board.add(row);
}
// occupiedCol[colIndex]:第 colIndex 列是否已有皇后
boolean[] occupiedCol = new boolean[n];
// antiDiag[row + colIndex]:副对角线 ↙(同行+列之和恒等,如 (0,3)(1,2)(2,1))
boolean[] antiDiag = new boolean[2 * n - 1];
// mainDiag[row - colIndex + n - 1]:主对角线 ↘(行-列之差恒等;+n-1 映到 [0, 2n-2])
boolean[] mainDiag = new boolean[2 * n - 1];
backtrack(board, 0, n, occupiedCol, antiDiag, mainDiag, ans);
return ans;
}
/**
* 在第 row 行放置一个皇后,并继续填后续行
*/
private void backtrack(List<StringBuilder> board, int row, int n,
boolean[] occupiedCol, boolean[] antiDiag, boolean[] mainDiag,
List<List<String>> ans) {
// 0..n-1 行都已放过:得到一种完整布局
if (row == n) {
List<String> snapshot = new ArrayList<>(n);
for (StringBuilder sb : board) {
snapshot.add(sb.toString());
}
ans.add(snapshot);
return;
}
for (int colIndex = 0; colIndex < n; colIndex++) {
int anti = row + colIndex; // 副对角下标
int main = row - colIndex + n - 1; // 主对角下标
// 该列或任一对角线已被占用则不能放
if (occupiedCol[colIndex] || antiDiag[anti] || mainDiag[main]) {
continue;
}
board.get(row).setCharAt(colIndex, 'Q');
occupiedCol[colIndex] = antiDiag[anti] = mainDiag[main] = true;
backtrack(board, row + 1, n, occupiedCol, antiDiag, mainDiag, ans);
// 撤掉本行该列的皇后及相关占用,尝试下一列
board.get(row).setCharAt(colIndex, '.');
occupiedCol[colIndex] = antiDiag[anti] = mainDiag[main] = false;
}
}
}
复杂度
- 时间:O(n! · n),近似按行选列并构造棋盘字符串
- 空间:O(n),递归深度与占用数组(不计答案)
11. 二分查找 (Binary Search)
核心思想: 在有序或「满足二段性」的答案空间上减半搜索。写清 left/right 开闭区间与循环不变量,避免死循环。
题目列表
35. 搜索插入位置 (Search Insert Position) ✅ 2026-08-14 标签:数组, 二分查找
74. 搜索二维矩阵 (Search a 2D Matrix) ✅ 2026-08-14 标签:数组, 二分查找, 矩阵
34. 在排序数组中查找元素的第一个和最后一个位置 (Find First and Last Position of Element in Sorted Array) ⭐ ✅ 2026-08-14 标签:数组, 二分查找
33. 搜索旋转排序数组 (Search in Rotated Sorted Array) ⭐ ✅ 2026-08-14 标签:数组, 二分查找
153. 寻找旋转排序数组中的最小值 (Find Minimum in Rotated Sorted Array) 🌟 ✅ 2026-08-14 标签:数组, 二分查找
4. 寻找两个正序数组的中位数 (Median of Two Sorted Arrays) ⭐ 🌟 ✅ 2026-08-14 标签:数组, 二分查找, 分治
35. 搜索插入位置 (Search Insert Position)
数组二分查找简单
题目
给定排序数组与目标值,在数组中找到目标值并返回其索引;若不存在,返回它按顺序将被插入的位置。必须使用 O(log n) 算法。
示例 1:
输入:nums = [1,3,5,6], target = 5
输出:2
示例 2:
输入:nums = [1,3,5,6], target = 2
输出:1
示例 3:
输入:nums = [1,3,5,6], target = 7
输出:4
提示:
1 <= nums.length <= 10^4-10^4 <= nums[i] <= 10^4nums为无重复元素的升序数组-10^4 <= target <= 10^4
思路简述
本质是找 第一个 ≥ target 的下标(lower bound)。闭区间二分:nums[mid] < target 时答案在右半,令 left = mid + 1;否则答案在左半含 mid,令 right = mid - 1。循环结束时 left 即为插入位置(若相等则已在循环内直接返回)。
参考代码
JAVA
class Solution {
public int searchInsert(int[] nums, int target) {
// left / right:闭区间 [left, right] 内可能存在 target,或插入点落在此范围及边界
int left = 0;
int right = nums.length - 1;
while (left <= right) {
// mid:防溢出写法,等价于 (left+right)/2
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid; // 题目保证无重复,找到即唯一下标
}
if (nums[mid] < target) {
// mid 及左侧都 < target,插入点 / 目标只可能在 mid 右侧
left = mid + 1;
} else {
// nums[mid] > target:mid 及右侧都太大,缩小到 mid 左侧
right = mid - 1;
}
}
// 未命中时:left 停在第一个 > target 的位置,也就是应按序插入的下标
// (此时 right == left - 1,区间已被掏空)
int ans = left;
return ans;
}
}
复杂度
- 时间:O(log n),每轮区间减半
- 空间:O(1)
74. 搜索二维矩阵 (Search a 2D Matrix)
数组二分查找矩阵中等
题目
给你满足下述两条属性的 m x n 整数矩阵:
- 每行从左到右非严格递增
- 每行第一个整数大于前一行最后一个整数
再给整数 target,若在矩阵中则返回 true,否则 false。须写出 O(log(m · n)) 解法。
示例 1:
输入:matrix =
[[1,3,5,7],[10,11,16,20],[23,30,34,60]],target = 3
输出:true
示例 2:
输入:matrix =
[[1,3,5,7],[10,11,16,20],[23,30,34,60]],target = 13
输出:false
提示:
m == matrix.length,n == matrix[i].length1 <= m, n <= 100-10^4 <= matrix[i][j], target <= 10^4
思路简述
因「下一行首 > 上一行尾」,按行优先展开后整体有序。把下标 mid ∈ [0, m·n) 映射为 r = mid / n、c = mid % n,在一维上做标准二分即可达到 O(log(m·n))。
参考代码
JAVA
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return false;
}
int m = matrix.length;
int n = matrix[0].length;
// left / right:把矩阵看成长度 m*n 的升序数组后的闭区间端点
int left = 0;
int right = m * n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
// 一维下标 mid → 二维坐标:每行 n 列,行号 mid/n,列号 mid%n
int r = mid / n;
int c = mid % n;
int val = matrix[r][c];
if (val == target) {
return true;
}
if (val > target) {
// mid 及右侧都 ≥ val > target,答案只可能在左侧
right = mid - 1;
} else {
left = mid + 1;
}
}
return false;
}
}
复杂度
- 时间:O(log(m · n)),对展开后的一维长度二分
- 空间:O(1)
34. 在排序数组中查找元素的第一个和最后一个位置 (Find First and Last Position of Element in Sorted Array)
数组二分查找中等
题目
给你按非递减顺序排列的整数数组 nums 与目标值 target,找出 target 在数组中的开始与结束位置。若不存在,返回 [-1, -1]。须设计 O(log n) 算法。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
提示:
0 <= nums.length <= 10^5-10^9 <= nums[i] <= 10^9nums是非递减数组-10^9 <= target <= 10^9
思路简述
分别二分两次:找最左 target、找最右 target。命中后若找左边界则继续往左半搜(right = mid - 1),找右边界则往右半搜(left = mid + 1),记下最近一次命中下标。找不到左边界则直接返回 [-1,-1]。等价于各做一次 下界 / 上界 查找。
参考代码
JAVA
class Solution {
public int[] searchRange(int[] nums, int target) {
// 先找最左出现位置;没有则整段不存在
int leftBound = findBound(nums, target, true);
if (leftBound == -1) {
return new int[]{-1, -1};
}
// 已知存在,再找最右出现位置
int rightBound = findBound(nums, target, false);
// ans:[起始下标, 结束下标]
int[] ans = new int[]{leftBound, rightBound};
return ans;
}
/**
* @param findFirst true:找第一个等于 target 的下标;false:找最后一个
* @return 找到则返回下标,否则 -1
*/
private int findBound(int[] nums, int target, boolean findFirst) {
int left = 0;
int right = nums.length - 1;
// found:二分过程中最近一次命中 target 的下标;可能被更左/更右的命中刷新
int found = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
found = mid;
if (findFirst) {
// 还想找更靠左的同值:收缩右边界,继续查左半
right = mid - 1;
} else {
// 还想找更靠右的同值:收缩左边界,继续查右半
left = mid + 1;
}
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return found;
}
}
复杂度
- 时间:O(log n),两次二分
- 空间:O(1)
33. 搜索旋转排序数组 (Search in Rotated Sorted Array)
数组二分查找中等
题目
整数数组 nums 原本按升序排列且值互不相同,在未知下标 k 处向左旋转后变为 [nums[k], …, nums[n-1], nums[0], …, nums[k-1]]。给定旋转后的 nums 与 target,若存在则返回下标,否则返回 -1。须 O(log n)。
示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
提示:
1 <= nums.length <= 5000-10^4 <= nums[i] <= 10^4nums中每个值独一无二- 数据保证数组曾在某下标旋转
-10^4 <= target <= 10^4
思路简述
旋转后任取 mid,左右至少有一半仍有序。用 nums[mid] 与 nums[left] 比较判断哪侧有序:若 target 落在有序那一段的数值范围内则缩到该侧,否则去另一侧。每轮仍丢掉一半,O(log n)。
参考代码
JAVA
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
}
// nums[left..mid] 整体升序(旋转断点不在这段内)
if (nums[mid] >= nums[left]) {
// target 落在左半有序区间 [nums[left], nums[mid]) 内 → 去左
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
// 否则目标在断点一侧 / 右半,丢掉左半
left = mid + 1;
}
} else {
// nums[mid..right] 整体升序(断点在 mid 左侧)
// target 落在右半有序区间 (nums[mid], nums[right]] 内 → 去右
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
}
复杂度
- 时间:O(log n)
- 空间:O(1)
153. 寻找旋转排序数组中的最小值 (Find Minimum in Rotated Sorted Array)
数组二分查找中等
题目
长度为 n 的升序数组经 1~n 次旋转后得到 nums(元素互不相同)。请找出并返回其中的最小元素。须 O(log n)。
旋转一次:[a[0], …, a[n-1]] → [a[n-1], a[0], …, a[n-2]]。
示例 1:
输入:nums = [3,4,5,1,2]
输出:1
解释:原数组 [1,2,3,4,5] 旋转 3 次。
示例 2:
输入:nums = [4,5,6,7,0,1,2]
输出:0
示例 3:
输入:nums = [11,13,15,17]
输出:11
解释:旋转 n 次后仍可能整体有序,最小在开头。
提示:
n == nums.length,1 <= n <= 5000-5000 <= nums[i] <= 5000- 所有整数互不相同
- 原为升序,并旋转了 1~n 次
思路简述
最小值在旋转断点处。用 nums[mid] 与 nums[right] 比较(不要和 nums[left] 比):若 nums[mid] > nums[right],说明断点在 mid 右侧,left = mid + 1;否则 mid 本身可能是最小值或断点在左侧,right = mid。循环用 left < right,结束时 left == right 即最小下标。
参考代码
JAVA
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
// 开闭:当 left == right 时区间只剩一个元素,必为最小值
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
// mid 落在较大的那段升序上,最小值一定在 mid 右边
// 必须和 right 比:若和 left 比,像 [4,5,6,7,0,1,2] 会误判「左半有序」
// 把 left 挪到 0 后,[0,1,2] 又与 left 同型,断点信息丢失
left = mid + 1;
} else {
// nums[mid] <= nums[right]:mid 右侧(含 right)有序,最小值在左半含 mid
// 故 right = mid(不能 mid-1,否则可能丢掉真正的最小值 mid)
right = mid;
}
}
// ans:最小元素
int ans = nums[left];
return ans;
}
}
复杂度
- 时间:O(log n)
- 空间:O(1)
4. 寻找两个正序数组的中位数 (Median of Two Sorted Arrays)
数组二分查找分治困难
题目
给定两个大小分别为 m、n 的正序(升序)数组 nums1、nums2,找出并返回这两个正序数组的中位数。算法时间复杂度应为 O(log(m + n))。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3],中位数 2。
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并 = [1,2,3,4],中位数 (2+3)/2 = 2.5。
提示:
nums1.length == m,nums2.length == n0 <= m <= 1000,0 <= n <= 10001 <= m + n <= 2000-10^6 <= nums1[i], nums2[i] <= 10^6
思路简述
不真正合并。在较短数组上二分切分点 i:从 nums1 取前 i 个、从 nums2 取前 j = totalLeft - i 个,凑成合并后的左半(共 (m+n+1)/2 个)。合法切分须满足 左半最大 ≤ 右半最小,即 nums1[i-1] ≤ nums2[j] 且 nums2[j-1] ≤ nums1[i]。奇数直接取左半最大;偶数取左最大与右最小的均值。
参考代码
JAVA
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 始终在较短数组上二分,保证 j = totalLeft - i 落在较长数组合法范围
if (nums1.length > nums2.length) {
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
int m = nums1.length;
int n = nums2.length;
// totalLeft:合并后左半应包含的元素个数;奇数时多带中间那个
int totalLeft = (m + n + 1) / 2;
// 二分的是「nums1 分给左半的个数 i」,范围 [0, m]
int low = 0;
int high = m;
while (low < high) {
// 取偏右的 mid,配合 low=i 避免死循环
int i = low + (high - low + 1) / 2;
int j = totalLeft - i; // nums2 分给左半的个数
// nums1 左半最后一个 > nums2 右半第一个 → i 太大,左半偏大
// (循环里 i≥1,故 i-1 安全;且 m≤n 保证 j 不会越界)
if (nums1[i - 1] > nums2[j]) {
high = i - 1;
} else {
// 当前 i 可行或偏小,尝试更大的 i
low = i;
}
}
int i = low;
int j = totalLeft - i;
// 边界用 ±∞ 哨兵:该侧没有元素时不影响 max/min
int nums1LeftMax = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
int nums1RightMin = (i == m) ? Integer.MAX_VALUE : nums1[i];
int nums2LeftMax = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
int nums2RightMin = (j == n) ? Integer.MAX_VALUE : nums2[j];
if ((m + n) % 2 == 1) {
// 奇数:中位数就是左半的最大值
return Math.max(nums1LeftMax, nums2LeftMax);
}
// 偶数:左半最大与右半最小的平均
return (Math.max(nums1LeftMax, nums2LeftMax)
+ Math.min(nums1RightMin, nums2RightMin)) / 2.0;
}
}
复杂度
- 时间:O(log(min(m, n))),只在较短数组上二分
- 空间:O(1)
12. 栈 (Stack)
核心思想: 后进先出,处理匹配、嵌套与「下一个更大/更小元素」(单调栈)。看到括号、解码、柱状图,优先想栈。
题目列表
20. 有效的括号 (Valid Parentheses) ⭐ ✅ 2026-08-14 标签:栈, 字符串
155. 最小栈 (Min Stack) 🌟 ✅ 2026-08-14 标签:栈, 设计
394. 字符串解码 (Decode String) 🌟 ✅ 2026-08-14 标签:栈, 递归, 字符串
739. 每日温度 (Daily Temperatures) ⭐ ✅ 2026-08-14 标签:栈, 数组, 单调栈
84. 柱状图中最大的矩形 (Largest Rectangle in Histogram) ⭐ 🌟 ✅ 2026-08-14 标签:栈, 数组, 单调栈
20. 有效的括号 (Valid Parentheses)
栈字符串简单
题目
给定只含 '('、')'、'{'、'}'、'['、']' 的字符串 s,判断是否有效。有效需满足:同类型闭合、正确顺序闭合、每个右括号都有对应左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([])"
输出:true
示例 5:
输入:s = "([)]"
输出:false
提示:
1 <= s.length <= 10^4s仅由括号'()[]{}'组成
思路简述
栈模拟匹配:遇左括号时压入 期望的右括号;遇右括号则弹出栈顶比较,空栈或类型不符即非法。扫完栈必须为空(不能有未闭合左括号)。
参考代码
JAVA
class Solution {
public boolean isValid(String s) {
// stack:存放「尚未匹配的左括号所期望的右括号」,栈顶对应最近未闭合的左括号
Deque<Character> stack = new ArrayDeque<>();
for (char ch : s.toCharArray()) {
if (ch == '(') {
stack.push(')'); // 之后必须先出现 ')' 才能闭合这对
} else if (ch == '[') {
stack.push(']');
} else if (ch == '{') {
stack.push('}');
} else {
// ch 是右括号:必须与栈顶期望一致;栈空说明没有对应左括号
if (stack.isEmpty() || stack.pop() != ch) {
return false;
}
}
}
// 栈空:所有左括号都已按正确顺序闭合
boolean ans = stack.isEmpty();
return ans;
}
}
复杂度
- 时间:O(n),单次扫描
- 空间:O(n),最坏全是左括号
155. 最小栈 (Min Stack)
栈设计中等
题目
设计支持 push、pop、top,并能在常数时间内取到最小元素的栈。实现:
MinStack():初始化void push(int val):入栈void pop():弹出栈顶int top():返回栈顶int getMin():返回当前最小值
示例 1:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"][[],[-2],[0],[-3],[],[],[],[]]
输出:[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); // -3
minStack.pop();
minStack.top(); // 0
minStack.getMin(); // -2
提示:
-2^31 <= val <= 2^31 - 1pop、top、getMin总在非空栈上调用- 各操作最多调用
3 * 10^4次
思路简述
双栈:data 存全部元素,minStack 存「每个前缀上的最小值」。push 时若 val ≤ 当前最小(或最小栈空)则同步压入;pop 时若弹出值等于最小栈顶则最小栈也弹。这样 getMin 直接 peek,O(1)。必须用 <=:相同最小值出现多次时,每份都要入最小栈,否则先弹掉一个后最小值会丢。
参考代码
JAVA
class MinStack {
// data:正常存所有入栈元素
private final Deque<Integer> data;
// minStack:栈顶始终是「当前 data 里的最小值」;仅在刷新最小值(含并列)时压入
private final Deque<Integer> minStack;
public MinStack() {
data = new ArrayDeque<>();
minStack = new ArrayDeque<>();
}
public void push(int val) {
data.push(val);
// 空,或 val 不大于当前最小:成为(或并列成为)新的最小值,记一层
if (minStack.isEmpty() || val <= minStack.peek()) {
minStack.push(val);
}
}
public void pop() {
int val = data.pop();
// 弹出的恰是当前最小值的一份拷贝:最小栈同步弹出,露出上一层最小值
if (!minStack.isEmpty() && minStack.peek().equals(val)) {
minStack.pop();
}
}
public int top() {
return data.peek();
}
public int getMin() {
return minStack.peek();
}
}
复杂度
- 时间:
push/pop/top/getMin均为 O(1) - 空间:O(n),最坏最小栈与数据栈同长(如非增序列)
394. 字符串解码 (Decode String)
栈递归字符串中等
题目
给定编码字符串,返回解码后的字符串。编码规则:k[encoded_string] 表示括号内内容重复 k 次(k 为正整数)。输入总有效、无多余空格、括号成对;数字只表示次数 k。输出长度不超过 10^5。
示例 1:
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
示例 2:
输入:s = "3[a2[c]]"
输出:"accaccacc"
示例 3:
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
示例 4:
输入:s = "abc3[cd]xyz"
输出:"abccdcdcdxyz"
提示:
1 <= s.length <= 30s由小写字母、数字与方括号组成,保证合法- 整数范围
[1, 300]
思路简述
一层括号对应一次「外层串 + 重复次数」的保存。扫到 [:把当前已拼好的串和累计的 k 入栈,然后清空,开始构造括号内层。扫到 ]:弹出外层串与次数,把 内层串重复 k 次接到外层后面,结果作为新的「当前串」。数字要按多位累加(count = count*10 + d);嵌套时栈自然处理。
参考代码
JAVA
class Solution {
public String decodeString(String s) {
// curr:当前层正在拼接的字符串(尚未被外层倍数包裹完)
StringBuilder curr = new StringBuilder();
// count:当前层 '[' 前的重复次数;可能是多位数,需边读边累加
int count = 0;
// countStack:进入更内层 '[' 时,暂存外层的重复次数
Deque<Integer> countStack = new ArrayDeque<>();
// strStack:进入更内层时,暂存外层已经拼好的前缀
Deque<StringBuilder> strStack = new ArrayDeque<>();
for (char ch : s.toCharArray()) {
if (Character.isDigit(ch)) {
// 多位数字:如 "12[a]" 中先读到 1 再读到 2 → 12
count = count * 10 + (ch - '0');
} else if (ch == '[') {
// 进入新一层:外层的「已有前缀」和「本层倍数」压栈保存
countStack.push(count);
strStack.push(curr);
// 重置,开始专门构造括号内部的串
count = 0;
curr = new StringBuilder();
} else if (ch == ']') {
// 内层 curr 构造完毕:接到外层前缀上,并重复 times 次
StringBuilder outer = strStack.pop();
int times = countStack.pop();
while (times-- > 0) {
outer.append(curr);
}
// 合并结果成为新的「当前层字符串」,继续往后扫
curr = outer;
} else {
// 普通字母,直接追加到当前层
curr.append(ch);
}
}
return curr.toString();
}
}
复杂度
- 时间:O(L),
L为解码后长度(每次 append 均摊到输出字符) - 空间:O(L),栈与当前串保存嵌套中间结果
739. 每日温度 (Daily Temperatures)
栈数组单调栈中等
题目
给定整数数组 temperatures 表示每天温度,返回数组 answer,其中 answer[i] 表示第 i 天之后下一个更高温度出现在几天后;若之后不会升高,则为 0。
示例 1:
输入:temperatures = [73,74,75,71,69,72,76,73]
输出:[1,1,4,2,1,1,0,0]
示例 2:
输入:temperatures = [30,40,50,60]
输出:[1,1,1,0]
示例 3:
输入:temperatures = [30,60,90]
输出:[1,1,0]
提示:
1 <= temperatures.length <= 10^530 <= temperatures[i] <= 100
思路简述
单调栈存「尚未找到更暖天气」的下标,保持栈内温度 单调不增。当前天更暖时,不断弹出栈顶:弹出下标 j 的答案就是 i - j。最后仍留在栈里的天答案保持默认 0。
参考代码
JAVA
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
// ans[i]:第 i 天要等几天才升温;默认 0 表示后面再也没有更高温
int[] ans = new int[n];
// stack:存下标;对应温度从栈底到栈顶单调不增(递减栈)
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// 当前温度比栈顶那天更高:栈顶那天的「下一个更高」就是今天
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int j = stack.pop();
ans[j] = i - j; // 间隔天数
}
// 今天也还没找到更暖的天,先入栈等后面来结算
stack.push(i);
}
return ans;
}
}
复杂度
- 时间:O(n),每个下标至多入栈、出栈一次
- 空间:O(n),栈最坏存下全部下标
84. 柱状图中最大的矩形 (Largest Rectangle in Histogram)
栈数组单调栈困难
题目
给定 n 个非负整数表示柱状图中各柱高度,每柱宽为 1、彼此相邻。求能勾勒出的矩形的最大面积。
示例 1:
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大矩形为图中红色区域,面积 10。
示例 2:
输入:heights = [2,4]
输出:4
提示:
1 <= heights.length <= 10^50 <= heights[i] <= 10^4
思路简述
对每根柱子,求「以它为高」能向左右延伸到的最大宽度,面积 = 高 × 宽,取全局最大。用单调递增栈存下标:当前柱更矮时,栈顶柱的右边界已确定,弹出并算面积。两端加高度为 0 的 哨兵,保证所有柱子最终都会被弹出结算。
参考代码
JAVA
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
// h:原数组左右各加一个高度 0 的哨兵
// 左哨兵:保证栈底有「比一切都矮」的柱,peek 永远安全
// 右哨兵:扫到末尾时强制把栈里剩余柱子全部弹出结算
int[] h = new int[n + 2];
System.arraycopy(heights, 0, h, 1, n);
// stack:存下标,对应高度严格递增(单调增栈)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(0); // 先压入左哨兵下标 0
int ans = 0;
for (int i = 1; i < n + 2; i++) {
// 当前柱比栈顶更矮:栈顶柱的「右侧第一个更矮柱」就是 i
while (!stack.isEmpty() && h[i] < h[stack.peek()]) {
int heightIndex = stack.pop();
// 左边界:弹出后新的栈顶(左侧第一个 ≤ 当前高 的柱,含哨兵)
// 可延伸宽度 = 右边界 i - 左边界 stack.peek() - 1
int width = i - stack.peek() - 1;
int area = width * h[heightIndex];
ans = Math.max(ans, area);
}
stack.push(i);
}
return ans;
}
}
复杂度
- 时间:O(n),每个下标至多入栈、出栈一次
- 空间:O(n),拷贝数组与栈
13. 堆 (Heap)
核心思想: 用优先队列维护 Top-K、动态极值;大小根堆可配合处理中位数等「动态数据流」问题。
题目列表
215. 数组中的第 K 个最大元素 (Kth Largest Element in an Array) ⭐ 🌟 ✅ 2026-08-14 标签:数组, 分治, 快速选择, 排序, 堆(优先队列)
347. 前 K 个高频元素 (Top K Frequent Elements) ✅ 2026-08-14 标签:数组, 哈希表, 分治, 桶排序, 计数, 快速选择, 排序, 堆(优先队列)
295. 数据流的中位数 (Find Median from Data Stream) ⭐ 🌟 ✅ 2026-08-14 标签:设计, 双指针, 数据流, 排序, 堆(优先队列)
215. 数组中的第 K 个最大元素 (Kth Largest Element in an Array)
数组分治快速选择排序堆(优先队列)中等
题目
给定整数数组 nums 与整数 k,返回数组中第 k 个最大的元素。注意是排序后的第 k 大,不是第 k 个不同的元素。须设计平均 O(n) 算法。
示例 1:
输入:nums = [3,2,1,5,6,4], k = 2
输出:5
示例 2:
输入:nums = [3,2,3,1,2,4,5,5,6], k = 4
输出:4
提示:
1 <= k <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4
思路简述
第 k 大 = 升序下标 n - k 的元素。用快速选择:随机选 pivot 划分,若 pivot 下标恰为 n-k 则找到;偏大只缩左半,偏小只缩右半。平均 O(n)。
维护大小为 k 的小根堆,扫完全组后堆顶即第 k 大;时间 O(n log k),也常写。
参考代码
JAVA
class Solution {
private static final Random RAND = new Random();
public int findKthLargest(int[] nums, int k) {
// targetIndex:升序排列后第 k 大所在下标(从 0 起)
int targetIndex = nums.length - k;
int left = 0;
int right = nums.length - 1;
while (true) {
// pivotIndex:本轮划分后 pivot 的最终下标
int pivotIndex = partition(nums, left, right);
if (pivotIndex == targetIndex) {
return nums[pivotIndex];
}
if (pivotIndex > targetIndex) {
// 目标在 pivot 左侧更小的一段
right = pivotIndex - 1;
} else {
left = pivotIndex + 1;
}
}
}
// 随机选 pivot,把区间排成「< pivot | pivot | > pivot」,返回 pivot 下标
private int partition(int[] nums, int left, int right) {
int pivotPos = left + RAND.nextInt(right - left + 1);
int pivot = nums[pivotPos];
swap(nums, pivotPos, left); // pivot 暂放到 left,方便双指针收拢
int i = left + 1;
int j = right;
while (i <= j) {
while (i <= j && nums[i] < pivot) {
i++; // 左侧应留下更小的,跳过已合法
}
while (i <= j && nums[j] > pivot) {
j--; // 右侧应留下更大的
}
if (i >= j) {
break;
}
swap(nums, i, j);
i++;
j--;
}
// j 停在最后一个 ≤ pivot 的位置,与 left 交换后 pivot 就位
swap(nums, left, j);
return j;
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
复杂度
- 时间:平均 O(n),最坏 O(n²)(随机 pivot 使最坏极罕见)
- 空间:O(1) 额外(原地划分)
347. 前 K 个高频元素 (Top K Frequent Elements)
数组哈希表分治桶排序计数快速选择排序堆(优先队列)中等
题目
给你整数数组 nums 与整数 k,返回其中出现频率前 k 高的元素。可按任意顺序返回答案。
示例 1:
输入:nums = [1,1,1,2,2,3], k = 2
输出:[1,2]
示例 2:
输入:nums = [1], k = 1
输出:[1]
示例 3:
输入:nums = [1,2,1,2,1,2,3,1,3,2], k = 2
输出:[1,2]
提示:
1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4k的范围是[1, 不同元素个数]- 答案唯一(前 k 高频集合唯一)
进阶:时间复杂度须优于 O(n log n)。
思路简述
先哈希统计频次,再用大小为 k 的 小根堆 按频次维护:堆顶是当前 Top-K 里最小的频次,超过 k 个就弹出堆顶。最终堆中即前 k 高频。时间 O(n log k),优于全排序的 O(n log n)。
频次最大为 n,可按频次分桶:bucket[freq] 存出现 freq 次的数,从高频往低频收集 k 个。
参考代码
JAVA
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// freq:数值 → 出现次数
Map<Integer, Integer> freq = new HashMap<>();
for (int num : nums) {
freq.put(num, freq.getOrDefault(num, 0) + 1);
}
// pq:小根堆,元素为 [数值, 频次],按频次比较;只保留频次最高的 k 个
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
for (Map.Entry<Integer, Integer> e : freq.entrySet()) {
pq.offer(new int[]{e.getKey(), e.getValue()});
if (pq.size() > k) {
pq.poll(); // 丢掉当前 Top-K 中频次最小的
}
}
int[] ans = new int[k];
for (int i = 0; i < k; i++) {
ans[i] = pq.poll()[0];
}
return ans;
}
}
复杂度
- 时间:O(n + m log k),
m为不同元素个数(≤ n);满足进阶优于 O(n log n)(当 k 较小) - 空间:O(m + k),哈希表与堆
295. 数据流的中位数 (Find Median from Data Stream)
设计双指针数据流排序堆(优先队列)困难
题目
中位数是有序列表中间值;偶数个则取中间两数平均值。实现 MedianFinder:
MedianFinder():初始化void addNum(int num):加入数据流中的整数double findMedian():返回目前所有元素的中位数(误差10^{-5}内可接受)
示例 1:
输入:
["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"][[], [1], [2], [], [3], []]
输出:[null, null, null, 1.5, null, 2.0]
解释:
MedianFinder medianFinder = new MedianFinder(); medianFinder.addNum(1); // [1] medianFinder.addNum(2); // [1,2] medianFinder.findMedian(); // 1.5 medianFinder.addNum(3); // [1,2,3] medianFinder.findMedian(); // 2.0
提示:
-10^5 <= num <= 10^5- 调用
findMedian前至少有一个元素 addNum/findMedian总调用次数至多5 * 10^4(以题面为准)
思路简述
对顶堆:大根堆 small 存较小的一半,小根堆 large 存较大的一半。始终保持 small 比 large 多 0 或 1 个,且 small 堆顶 ≤ large 堆顶。奇数个时中位数是 small 堆顶;偶数个取两堆顶平均。
参考代码
JAVA
class MedianFinder {
// small:较小半边,大根堆;堆顶是较小半边的最大值(偏中位数左侧)
private final PriorityQueue<Integer> small;
// large:较大半边,小根堆;堆顶是较大半边的最小值(偏中位数右侧)
private final PriorityQueue<Integer> large;
public MedianFinder() {
small = new PriorityQueue<>((a, b) -> b - a);
large = new PriorityQueue<>();
}
public void addNum(int num) {
// 先丢进较小半边,再把其中最大的挪到较大半边,保证分界有序
small.offer(num);
large.offer(small.poll());
// 平衡个数:允许 small 比 large 多一个(奇数时中位数在 small)
if (large.size() > small.size()) {
small.offer(large.poll());
}
}
public double findMedian() {
if (small.size() == large.size()) {
// 偶数:两堆顶是中间相邻两数
return (small.peek() + large.peek()) / 2.0;
}
// 奇数:多出的那一个在 small 堆顶
return small.peek();
}
}
复杂度
- 时间:
addNumO(log n),findMedianO(1) - 空间:O(n),保存全部元素
14. 贪心 (Greedy)
核心思想: 每一步做局部最优选择,并证明(或按题意确信)能导向全局最优。常见于区间覆盖、跳跃可达、股票一次买卖等。
题目列表
121. 买卖股票的最佳时机 (Best Time to Buy and Sell Stock) ⭐ ✅ 2026-08-14 标签:数组, 动态规划
55. 跳跃游戏 (Jump Game) ⭐ ✅ 2026-08-14 标签:贪心, 数组, 动态规划
45. 跳跃游戏 II (Jump Game II) 🌟 ✅ 2026-08-14 标签:贪心, 数组, 动态规划
763. 划分字母区间 (Partition Labels) ✅ 2026-08-14 标签:贪心, 哈希表, 双指针, 字符串
121. 买卖股票的最佳时机 (Best Time to Buy and Sell Stock)
数组动态规划简单
题目
给定数组 prices,prices[i] 是第 i 天股价。只能选择某一天买入,并在之后某一天卖出,计算能获得的最大利润;若无法盈利则返回 0。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:5
解释:第 2 天买入(价 1),第 5 天卖出(价 6),利润 5。不能 7-1=6(卖出须在买入之后)。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:持续下跌,无法完成盈利交易。
提示:
1 <= prices.length <= 10^50 <= prices[i] <= 10^4
思路简述
一遍扫描:维护到目前为止的 历史最低买入价,每天尝试「今天卖出」的利润 price - minPrice,取全局最大;跌破最低价则更新买入价。
参考代码
JAVA
class Solution {
public int maxProfit(int[] prices) {
// minPrice:截至前一天(含)见过的最低价,作为潜在买入点
int minPrice = prices[0];
// ans:全局最大利润;至少为 0(可以选择不交易)
int ans = 0;
for (int price : prices) {
if (price < minPrice) {
minPrice = price; // 更低的买入点,后面卖出更划算
}
// 假设今天卖出:利润 = 今日价 - 历史最低买入价
ans = Math.max(ans, price - minPrice);
}
return ans;
}
}
复杂度
- 时间:O(n),单次遍历
- 空间:O(1)
55. 跳跃游戏 (Jump Game)
贪心数组动态规划中等
题目
给你非负整数数组 nums,最初在第一个下标。每个元素表示该位置可跳跃的最大长度。判断能否到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:下标 0→1→末尾。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:总会停在下标 3,而nums[3]=0,无法再前进。
提示:
1 <= nums.length <= 10^40 <= nums[i] <= 10^5
思路简述
维护当前能到达的最远下标 maxReach。遍历时只处理 i ≤ maxReach 的位置,并用 i + nums[i] 更新最远。若 maxReach 盖过末尾 则成功;若走到 i == maxReach 仍无法前进则失败。
参考代码
JAVA
class Solution {
public boolean canJump(int[] nums) {
// maxReach:从已访问过的格子出发,目前能跳到的最远下标
int maxReach = 0;
int n = nums.length;
for (int i = 0; i < n; i++) {
// 从 i 起跳,最远可到 i + nums[i]
maxReach = Math.max(maxReach, i + nums[i]);
if (maxReach >= n - 1) {
return true; // 已经能覆盖最后一个下标
}
// i 走到了当前最远边界,且还没到终点:后面格子都够不着
if (i >= maxReach) {
return false;
}
}
return true;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
45. 跳跃游戏 II (Jump Game II)
贪心数组动态规划中等
题目
长度为 n 的数组 nums,从下标 0 出发;nums[i] 表示从 i 最多能往后跳的长度。返回到达下标 n - 1 的最小跳跃次数。测试用例保证可以到达。
示例 1:
输入:nums = [2,3,1,1,4]
输出:2
解释:0→1→末尾,共 2 次跳跃。
示例 2:
输入:nums = [2,3,0,1,4]
输出:2
提示:
1 <= nums.length <= 10^40 <= nums[i] <= 1000- 题目保证可以到达
n - 1
思路简述
在「当前这一跳覆盖的区间」内扫一遍,用 maxReach 记录该区间内起跳能到达的最远位置。当 i 走到本跳右边界 end 时,必须再跳一次,并把 end 更新为 maxReach。只需遍历到倒数第二个下标(已在终点不必再跳)。本质是 按层扩展 BFS:每一跳对应一层,层边界更新一次计数 +1。
参考代码
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)
763. 划分字母区间 (Partition Labels)
贪心哈希表双指针字符串中等
题目
给你字符串 s,要划分成尽可能多的片段,使得同一字母最多出现在一个片段中;各片段按序拼接仍等于 s。返回每个片段的长度列表。
示例 1:
输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:划分为"ababcbaca"、"defegde"、"hijhklij"。
示例 2:
输入:s = "eccbbbbdec"
输出:[10]
提示:
1 <= s.length <= 500s仅由小写英文字母组成
思路简述
先记录每个字母最后一次出现的下标。再从左往右扫:用 maxReach 维护「当前片段内已出现字母」的最远结束位置。当扫描指针 i 走到 maxReach 时,说明该片段内所有字母都不会再出现在后面,可以在此切一刀。贪心目标是 在满足「字母不跨段」的前提下尽早切分。
参考代码
JAVA
class Solution {
public List<Integer> partitionLabels(String s) {
// lastIndex[c - 'a']:字母 c 在 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++) {
char ch = s.charAt(i);
// 片段内一旦出现 ch,就必须覆盖到它的最后一次出现
maxReach = Math.max(maxReach, lastIndex[ch - 'a']);
if (i == maxReach) {
// 走到本片段右端:中间出现过的字母都不会再出现在后面
ans.add(i - left + 1);
left = maxReach + 1; // 下一段从右端的下一个位置开始
}
}
return ans;
}
}
复杂度
- 时间:O(n),两遍线性扫描
- 空间:O(1),
lastIndex固定 26;答案列表不计额外算法空间时可视为 O(1)
15. 动态规划 (DP)
核心思想: 定义状态 dp[i](或少量滚动变量),找转移方程与初始化。先想「子问题」,再写递推,最后考虑空间压缩。
题目列表
70. 爬楼梯 (Climbing Stairs) ⭐ ✅ 2026-08-14 标签:记忆化, 数学, 动态规划
118. 杨辉三角 (Pascal's Triangle) ✅ 2026-08-14 标签:数组, 动态规划
198. 打家劫舍 (House Robber) ⭐ ✅ 2026-08-14 标签:数组, 动态规划
279. 完全平方数 (Perfect Squares) ✅ 2026-08-14 标签:广度优先搜索, 数学, 动态规划
322. 零钱兑换 (Coin Change) ⭐ ✅ 2026-08-14 标签:广度优先搜索, 数组, 动态规划
139. 单词拆分 (Word Break) 🌟 ✅ 2026-08-14 标签:字典树, 记忆化, 数组, 哈希表, 字符串, 动态规划
300. 最长递增子序列 (Longest Increasing Subsequence) ⭐ ✅ 2026-08-14 标签:数组, 二分查找, 动态规划
152. 乘积最大子数组 (Maximum Product Subarray) 🌟 ✅ 2026-08-14 标签:数组, 动态规划
416. 分割等和子集 (Partition Equal Subset Sum) ✅ 2026-08-14 标签:数组, 动态规划
32. 最长有效括号 (Longest Valid Parentheses) 🌟 ✅ 2026-08-14 标签:栈, 字符串, 动态规划
70. 爬楼梯 (Climbing Stairs)
记忆化数学动态规划简单
题目
爬到楼顶需要 n 阶,每次可爬 1 或 2 阶。问有多少种不同方法爬到楼顶。
示例 1:
输入:n = 2
输出:2
解释:1+1或2。
示例 2:
输入:n = 3
输出:3
解释:1+1+1、1+2、2+1。
提示:
1 <= n <= 45
思路简述
到达第 i 阶只能从 i-1(再爬 1)或 i-2(再爬 2)过来,故 f(i) = f(i-1) + f(i-2),即斐波那契。用两个滚动变量代替整表即可。
参考代码
JAVA
class Solution {
public int climbStairs(int n) {
if (n <= 2) {
return n; // 1 阶只有 1 种;2 阶有 1+1 / 2 两种
}
int prev1 = 1; // f(i-2):爬到 i-2 阶的方法数
int prev2 = 2; // f(i-1):爬到 i-1 阶的方法数
int ans = 0;
for (int i = 3; i <= n; i++) {
ans = prev2 + prev1; // f(i) = f(i-1) + f(i-2)
prev1 = prev2;
prev2 = ans;
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
118. 杨辉三角 (Pascal's Triangle)
数组动态规划简单
题目
给定非负整数 numRows,生成杨辉三角的前 numRows 行。每个数是它左上方和右上方的数之和。
示例 1:
输入:numRows = 5
输出:[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入:numRows = 1
输出:1
提示:
1 <= numRows <= 30
思路简述
逐行构造:每行两端固定为 1;中间第 j 个元素等于上一行的 prev[j-1] + prev[j](左上 + 右上)。预分配容量避免扩容。
参考代码
JAVA
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> ans = new ArrayList<>(numRows); // 预分配行数容量
ans.add(List.of(1)); // 第 0 行只有 [1]
for (int i = 1; i < numRows; i++) {
List<Integer> row = new ArrayList<>(i + 1); // 第 i 行有 i+1 个数
row.add(1); // 左端始终为 1
// 中间:取上一行左上 prev[j-1] 与右上 prev[j] 之和
for (int j = 1; j < i; j++) {
row.add(ans.get(i - 1).get(j - 1) + ans.get(i - 1).get(j));
}
row.add(1); // 右端始终为 1
ans.add(row);
}
return ans;
}
}
复杂度
- 时间:O(numRows²),第 i 行约写 i 个元素
- 空间:O(1) 额外(不含答案本身);答案规模为 O(numRows²)
198. 打家劫舍 (House Robber)
数组动态规划中等
题目
沿街房屋各有现金,相邻房屋不能同夜偷窃(否则报警)。给定非负数组 nums,求一夜能偷到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷 1 号与 3 号,1 + 3 = 4。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷 1、3、5 号,2 + 9 + 1 = 12。
提示:
1 <= nums.length <= 1000 <= nums[i] <= 400
思路简述
对第 i 间房:偷则金额为「前 i-2 间最优 + nums[i]」;不偷则继承「前 i-1 间最优」。转移为 dp[i] = max(dp[i-2] + nums[i], dp[i-1]),滚动两个变量即可。
参考代码
JAVA
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
// prev1 = dp[i-2]:偷到第 i-2 间为止的最高金额
// prev2 = dp[i-1]:偷到第 i-1 间为止的最高金额
int prev1 = nums[0];
int prev2 = Math.max(nums[0], nums[1]);
int ans = prev2;
for (int i = 2; i < nums.length; i++) {
// 偷第 i 间:prev1 + nums[i];不偷:prev2
ans = Math.max(prev1 + nums[i], prev2);
prev1 = prev2;
prev2 = ans;
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
279. 完全平方数 (Perfect Squares)
广度优先搜索数学动态规划中等
题目
给你整数 n,返回和为 n 的完全平方数的最少数量。完全平方数如 1、4、9、16…
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 10^4
思路简述
完全背包:把 1², 2², … 看作可无限使用的「物品」。dp[i] 表示凑出和 i 所需最少平方个数,转移为对每个 j² ≤ i:dp[i] = min(dp[i], dp[i - j²] + 1)。
参考代码
JAVA
class Solution {
public int numSquares(int n) {
// dp[i]:和为 i 时,完全平方数的最少个数
int[] dp = new int[n + 1];
Arrays.fill(dp, n + 1); // 用不可能达到的上界当「未算出」哨兵(最多拆成 n 个 1)
dp[0] = 0; // 和为 0 不需要任何平方数
for (int i = 1; i <= n; i++) {
for (int j = 1; j * j <= i; j++) {
// 选一个 j²,剩余 i-j² 的最优再 +1
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
int ans = dp[n];
return ans;
}
}
复杂度
- 时间:O(n · √n),外层 n,内层最多 √i 次
- 空间:O(n)
322. 零钱兑换 (Coin Change)
广度优先搜索数组动态规划中等
题目
给定不同面额硬币数组 coins 与总金额 amount,每种硬币无限枚。返回凑成 amount 的最少硬币数;无法凑成则返回 -1。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
提示:
1 <= coins.length <= 121 <= coins[i] <= 2^31 - 10 <= amount <= 10^4
思路简述
完全背包:外层枚举硬币、内层从小到大枚举金额,保证同种硬币可多次使用。dp[j] 表示凑出金额 j 的最少枚数,转移 dp[j] = min(dp[j], dp[j - coin] + 1);最终若仍为哨兵则无解。
参考代码
JAVA
class Solution {
public int coinChange(int[] coins, int amount) {
if (amount == 0) {
return 0;
}
// dp[j]:凑出金额 j 所需的最少硬币数
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // 哨兵:比「全用 1 元」还大,表示尚不可达
dp[0] = 0;
for (int coin : coins) {
// 从小到大:同一枚硬币可被多次计入(完全背包)
for (int j = coin; j <= amount; j++) {
dp[j] = Math.min(dp[j], dp[j - coin] + 1);
}
}
int ans = (dp[amount] == amount + 1) ? -1 : dp[amount];
return ans;
}
}
复杂度
- 时间:O(n · amount),
n为硬币种数 - 空间:O(amount)
139. 单词拆分 (Word Break)
字典树记忆化数组哈希表字符串动态规划中等
题目
给定字符串 s 与字典 wordDict。若能用字典中一个或多个单词(可重复)拼接出 s,返回 true。
示例 1:
输入:s = "leetcode", wordDict = ["leet", "code"]
输出:true
解释:"leet" + "code"
示例 2:
输入:s = "applepenapple", wordDict = ["apple", "pen"]
输出:true
解释:单词可重复使用。
示例 3:
输入:s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:false
提示:
1 <= s.length <= 3001 <= wordDict.length <= 10001 <= wordDict[i].length <= 20s与wordDict[i]仅含小写字母;字典词互不相同
思路简述dp[i] 表示前缀 s[0..i) 能否拆分。枚举切分点 j:若 dp[j] 为真且 s[j..i) 在字典中,则 dp[i] = true。字典放进 HashSet 加速查询。
参考代码
JAVA
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
Set<String> dict = new HashSet<>(wordDict);
// dp[i]:前缀 s[0..i)(前 i 个字符)能否被字典拆分
boolean[] dp = new boolean[n + 1];
dp[0] = true; // 空前缀视为可拆分
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
// 左段可拆分,且右段 s[j..i) 是一个字典词(substring 左闭右开)
if (dp[j] && dict.contains(s.substring(j, i))) {
dp[i] = true;
break; // 找到一种切法即可
}
}
}
boolean ans = dp[n];
return ans;
}
}
复杂度
- 时间:O(n² · L),
L为substring/ 哈希查询相关长度开销;粗估 O(n²) - 空间:O(n + Σ|word|),DP 数组与字典集合
300. 最长递增子序列 (Longest Increasing Subsequence)
数组二分查找动态规划中等
题目
给定整数数组 nums,求最长严格递增子序列的长度。子序列可删除元素,但相对顺序不变。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:LIS 为[2,3,7,101]。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500-10^4 <= nums[i] <= 10^4
进阶:将时间复杂度降到 O(n log n)。
思路简述dp[i] 表示以 nums[i] 结尾的 LIS 长度,初值均为 1。枚举所有 j < i,若 nums[i] > nums[j],则可接在其后:dp[i] = max(dp[i], dp[j] + 1)。全局取 dp 最大值。
参考代码
JAVA
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
// dp[i]:以 nums[i] 结尾的最长严格递增子序列长度
int[] dp = new int[n];
Arrays.fill(dp, 1); // 单个元素自身构成长度为 1 的子序列
int ans = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
// 只有严格大于才能接在 j 后面
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
ans = Math.max(ans, dp[i]);
}
return ans;
}
}
复杂度
- 时间:O(n²)
- 空间:O(n)
维护数组 tails:tails[len-1] 表示长度为 len 的递增子序列中结尾最小的那个数。遍历每个 num:若比 tails 末尾大则追加;否则二分找到第一个 ≥ num 的位置替换。最终 tails 长度即为 LIS 长度(tails 本身不一定是某条真实子序列)。
JAVA
class Solution {
public int lengthOfLIS(int[] nums) {
// tails[k]:长度为 k+1 的所有递增子序列中,结尾最小的值
int[] tails = new int[nums.length];
int size = 0; // 当前 tails 有效长度
for (int num : nums) {
int left = 0, right = size;
// 找第一个 >= num 的位置(下界)
while (left < right) {
int mid = left + (right - left) / 2;
if (tails[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
tails[left] = num;
if (left == size) {
size++; // 扩展了最长长度
}
}
return size;
}
}
152. 乘积最大子数组 (Maximum Product Subarray)
数组动态规划中等
题目
给定整数数组 nums,求乘积最大的非空连续子数组对应的乘积(答案保证为 32 位整数)。
示例 1:
输入:nums = [2,3,-2,4]
输出:6
解释:子数组[2,3]乘积为 6。
示例 2:
输入:nums = [-2,0,-1]
输出:0
解释:[-2,-1]不连续,不是子数组。
提示:
1 <= nums.length <= 2 · 10^4-10 <= nums[i] <= 10- 任意子数组乘积保证是 32 位整数
思路简述
同时维护以当前位置结尾的最大乘积 imax 与最小乘积 imin(负得越多,再乘负数可能变最大)。遇到负数时先交换二者,再更新:imax = max(num, imax·num),imin = min(num, imin·num);全局记录 imax 的最大值。
参考代码
JAVA
class Solution {
public int maxProduct(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
// imax / imin:以当前下标结尾的子数组乘积的最大 / 最小值
int imax = nums[0];
int imin = nums[0];
int ans = nums[0];
for (int i = 1; i < nums.length; i++) {
int num = nums[i];
if (num < 0) {
// 乘负数后:原最大变最小、原最小变最大,先交换再统一更新
int temp = imax;
imax = imin;
imin = temp;
}
// 也可以从当前元素重新开始一段子数组
imax = Math.max(num, imax * num);
imin = Math.min(num, imin * num);
ans = Math.max(ans, imax);
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
416. 分割等和子集 (Partition Equal Subset Sum)
数组动态规划中等
题目
给定只含正整数的非空数组 nums,判断能否分成两个子集,使两子集元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:可分成[1,5,5]与[11]。
示例 2:
输入:nums = [1,2,3,5]
输出:false
提示:
1 <= nums.length <= 2001 <= nums[i] <= 100
思路简述
若总和为奇数则不可能均分。目标变为:能否选出若干数使其和恰好为 total/2。0-1 背包:dp[j] 表示能否凑出和 j;对每个数从大到小更新,保证每个数只用一次:dp[j] = dp[j] || dp[j - num]。
参考代码
JAVA
class Solution {
public boolean canPartition(int[] nums) {
int total = 0;
for (int num : nums) {
total += num;
}
if (total % 2 != 0) {
return false; // 奇数无法均分成两个相等和
}
int target = total / 2; // 等价于:能否选出子集和为 target
// dp[j]:是否存在子集,元素和恰好为 j
boolean[] dp = new boolean[target + 1];
dp[0] = true; // 空子集和为 0
for (int num : nums) {
// 倒序:同一轮里每个 num 最多用一次(0-1 背包)
for (int j = target; j >= num; j--) {
// 不选 num 保持 dp[j];选 num 则看 j-num 是否可达
dp[j] = dp[j] || dp[j - num];
}
}
boolean ans = dp[target];
return ans;
}
}
复杂度
- 时间:O(n · target),
target = total/2 - 空间:O(target)
32. 最长有效括号 (Longest Valid Parentheses)
栈字符串动态规划困难
题目
给定只含 '(' 与 ')' 的字符串,求最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效子串是"()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效子串是"()()"
示例 3:
输入:s = ""
输出:0
提示:
0 <= s.length <= 3 · 10^4s[i]为'('或')'
思路简述
双向计数。从左往右:left/right 统计 '('/')';只有 left == right 时当前段才是合法子串,用 2·right 更新答案;right > left 则多出的 ')' 已无法匹配,清零重开。left > right 时先不要更新(例如 "()(()" 若按 left >= right 会误记成 4)。仅左扫会漏掉「前面多 '('、后面才配平」的最长段,故再 从右往左对称扫一遍(把 ')' 当开口、'(' 当闭合)。
参考代码
JAVA
class Solution {
public int longestValidParentheses(String s) {
int n = s.length();
int ans = 0;
int left = 0; // 当前段内 '(' 个数(右扫时改为 ')' 个数)
int right = 0; // 当前段内 ')' 个数(右扫时改为 '(' 个数)
// 从左到右:处理「右边多 ')'」导致断开的情况
for (int i = 0; i < n; i++) {
if (s.charAt(i) == '(') {
left++;
} else {
right++;
}
if (left < right) {
// ')' 比 '(' 多,无法再配平,重新开始
left = right = 0;
} else if (left == right) {
// 恰好配平才是合法连续子串
ans = Math.max(ans, right * 2);
}
// left > right:前面多 '(',先继续数,等右扫再处理
}
// 从右到左:处理「左边多 '('」导致左扫算不全的情况
left = right = 0;
for (int i = n - 1; i >= 0; i--) {
if (s.charAt(i) == ')') {
left++; // 从右看,')' 充当开口
} else {
right++; // '(' 充当闭合
}
if (left < right) {
left = right = 0;
} else if (left == right) {
ans = Math.max(ans, right * 2);
}
}
return ans;
}
}
复杂度
- 时间:O(n),两遍扫描
- 空间:O(1)
"()(()" 左扫过程中会出现 left=3, right=2。若此时用 2 * right = 4 更新,会把不连续的两段括号算在一起。必须等 left == right 才更新。
16. 多维动态规划 (Multidimensional DP)
核心思想: 状态通常带二维下标(网格位置、两个串的前缀等)。先画小表格找转移,再落代码;注意边界初始化。
题目列表
62. 不同路径 (Unique Paths) ✅ 2026-08-14 标签:数学, 动态规划, 组合数学
64. 最小路径和 (Minimum Path Sum) ⭐ ✅ 2026-08-14 标签:数组, 动态规划, 矩阵
5. 最长回文子串 (Longest Palindromic Substring) ⭐ 🌟 ✅ 2026-08-14 标签:双指针, 字符串, 动态规划
1143. 最长公共子序列 (Longest Common Subsequence) ⭐ ✅ 2026-08-14 标签:字符串, 动态规划
72. 编辑距离 (Edit Distance) ⭐ ✅ 2026-08-14 标签:字符串, 动态规划
62. 不同路径 (Unique Paths)
数学动态规划组合数学中等
题目
机器人在 m × n 网格左上角,每次只能向右或向下走一步,求到达右下角的不同路径数。
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:右→下→下;下→下→右;下→右→下。
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
提示:
1 <= m, n <= 100- 答案保证 ≤
2 · 10^9
思路简述dp[i][j] = dp[i-1][j] + dp[i][j-1](从上或从左来)。第一行/第一列只有 1 种走法。滚动成一维:dp[j] 表示当前行第 j 列,更新时 dp[j] += dp[j-1](旧 dp[j] 是上方,dp[j-1] 是左方)。
参考代码
JAVA
class Solution {
public int uniquePaths(int m, int n) {
// dp[j]:当前处理到某一行时,到达第 j 列的路径数
int[] dp = new int[n];
Arrays.fill(dp, 1); // 第一行全为 1;且每行第 0 列始终为 1(只会一直向下)
for (int i = 1; i < m; i++) { // 从第 2 行开始
for (int j = 1; j < n; j++) {
// dp[j](尚未更新)= 上一行同列;dp[j-1] = 本行左列
dp[j] = dp[j] + dp[j - 1];
}
}
int ans = dp[n - 1];
return ans;
}
}
复杂度
- 时间:O(m · n)
- 空间:O(n)
64. 最小路径和 (Minimum Path Sum)
数组动态规划矩阵中等
题目
给定含非负整数的 m × n 网格 grid,从左上走到右下,每次只能向下或向右,求路径数字和的最小值。
示例 1:
输入:grid =
[[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:路径1→3→1→1→1的和最小。
示例 2:
输入:grid =
[[1,2,3],[4,5,6]]
输出:12
提示:
m == grid.length,n == grid[i].length1 <= m, n <= 2000 <= grid[i][j] <= 200
思路简述
原地 DP:grid[i][j] 累加成「走到该格的最小路径和」。第一行只能从左累加,第一列只能从上累加;其余格子取 左、上两者较小值再加上自身。
参考代码
JAVA
class Solution {
public int minPathSum(int[][] grid) {
int row = grid.length;
int col = grid[0].length;
// 第一行:只能一直向右,前缀累加
for (int j = 1; j < col; j++) {
grid[0][j] += grid[0][j - 1];
}
// 第一列:只能一直向下,前缀累加
for (int i = 1; i < row; i++) {
grid[i][0] += grid[i - 1][0];
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
// 从左或从上走来,选路径和更小的那条
grid[i][j] += Math.min(grid[i][j - 1], grid[i - 1][j]);
}
}
int ans = grid[row - 1][col - 1];
return ans;
}
}
复杂度
- 时间:O(m · n)
- 空间:O(1) 额外(原地修改
grid)
5. 最长回文子串 (Longest Palindromic Substring)
双指针字符串动态规划中等
题目
给定字符串 s,找出其中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba"也是正确答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
提示:
1 <= s.length <= 1000s仅由数字和英文字母组成
思路简述dp[i][j] 表示 s[i..j] 是否回文。按子串长度 L 从小到大枚举,保证转移时更短的内层已算好:首尾字符不同则否;长度 ≤ 2(aa)或去首尾后 dp[i+1][j-1] 为真则是。维护最长段的起点与长度。关键转移:首尾相同且(短串或内层回文)。
参考代码
JAVA
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if (n < 2) {
return s;
}
int maxLen = 1; // 当前最长回文长度
int begin = 0; // 最长回文的起始下标
// dp[i][j]:子串 s[i..j](含两端)是否为回文
boolean[][] dp = new boolean[n][n];
for (int i = 0; i < n; i++) {
dp[i][i] = true; // 单字符必是回文
}
// 按长度 L 枚举,保证算 dp[i][j] 时 dp[i+1][j-1] 已就绪
for (int L = 2; L <= n; L++) {
for (int i = 0; i < n; i++) {
int j = i + L - 1; // 右端:使区间长度为 L
if (j >= n) {
break;
}
if (s.charAt(i) != s.charAt(j)) {
dp[i][j] = false;
} else if (j - i < 3) {
// 长度 2(aa)或 3(aba):首尾相同即回文
dp[i][j] = true;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
if (dp[i][j] && L > maxLen) {
maxLen = L;
begin = i;
}
}
}
String ans = s.substring(begin, begin + maxLen);
return ans;
}
}
复杂度
- 时间:O(n²)
- 空间:O(n²)
1143. 最长公共子序列 (Longest Common Subsequence)
字符串动态规划中等
题目
给定两个字符串 text1、text2,返回它们最长公共子序列的长度;不存在则返回 0。子序列可删除字符,但相对顺序不变。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是"ace"。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
提示:
1 <= text1.length, text2.length <= 1000- 仅含小写英文字母
思路简述dp[i][j] 表示 text1 前 i 个字符与 text2 前 j 个字符的 LCS 长度。若当前字符相同,可接在更短前缀后面:dp[i][j] = dp[i-1][j-1] + 1;否则只能丢弃一侧:max(dp[i-1][j], dp[i][j-1])。第 0 行/列为空串,LCS 为 0。
参考代码
JAVA
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
// dp[i][j]:text1[0..i) 与 text2[0..j) 的最长公共子序列长度
int[][] dp = new int[m + 1][n + 1]; // 默认 0:空串与任意串的 LCS 为 0
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
// 当前字符可纳入公共子序列
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// 不相等:丢掉 text1 末尾 或 丢掉 text2 末尾,取更长者
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
int ans = dp[m][n];
return ans;
}
}
复杂度
- 时间:O(m · n)
- 空间:O(m · n)
72. 编辑距离 (Edit Distance)
字符串动态规划中等
题目
给定单词 word1、word2,返回将 word1 转成 word2 的最少操作数。可进行:插入、删除、替换一个字符。
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:horse → rorse → rose → ros
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
提示:
0 <= word1.length, word2.length <= 500- 仅含小写英文字母
思路简述dp[i][j] 表示把 word1 前 i 个字符变成 word2 前 j 个字符的最少操作。字符相同则不操作,继承 dp[i-1][j-1];否则在删除、插入、替换三者中取最小:min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1。边界:空串变成长度 k 的串需要 k 次插入(或反过来全删除)。
参考代码
JAVA
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// dp[i][j]:word1[0..i) 变成 word2[0..j) 的最少操作数
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
dp[i][0] = i; // word2 为空:删除 word1 的 i 个字符
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j; // word1 为空:插入 word2 的 j 个字符
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1]; // 相同:无需操作
} else {
int del = dp[i - 1][j] + 1; // 删除 word1[i-1]
int ins = dp[i][j - 1] + 1; // 插入 word2[j-1]
int rep = dp[i - 1][j - 1] + 1; // 把 word1[i-1] 替换成 word2[j-1]
dp[i][j] = Math.min(del, Math.min(ins, rep));
}
}
}
int ans = dp[m][n];
return ans;
}
}
复杂度
- 时间:O(m · n)
- 空间:O(m · n)
17. 技巧 (Tricks)
核心思想: Hot 100 里偏「巧解」的一组:位运算、投票、双指针分区、原地置换等。掌握套路后,很多题能直接套模板。
题目列表
136. 只出现一次的数字 (Single Number) ✅ 2026-08-14 标签:位运算, 数组
169. 多数元素 (Majority Element) ✅ 2026-08-14 标签:数组, 哈希表, 分治, 计数, 排序
75. 颜色分类 (Sort Colors) ✅ 2026-08-14 标签:数组, 双指针, 排序
31. 下一个排列 (Next Permutation) 🌟 ✅ 2026-08-14 标签:数组, 双指针
287. 寻找重复数 (Find the Duplicate Number) ⭐ ✅ 2026-08-14 标签:位运算, 数组, 双指针, 二分查找
136. 只出现一次的数字 (Single Number)
位运算数组简单
题目
非空数组 nums 中,除某个元素只出现一次外,其余均出现两次。找出那个只出现一次的元素。要求线性时间、常数额外空间。
示例 1:
输入:nums = [2,2,1]
输出:1
示例 2:
输入:nums = [4,1,2,1,2]
输出:4
示例 3:
输入:nums = [1]
输出:1
提示:
1 <= nums.length <= 3 · 10^4-3 · 10^4 <= nums[i] <= 3 · 10^4- 除一个元素外,其余均出现两次
思路简述
异或满足交换律、结合律,且 a ⊕ a = 0、a ⊕ 0 = a。全部异或后,成对数字抵消,剩下的就是只出现一次的那个:ans ^= num。
参考代码
JAVA
class Solution {
public int singleNumber(int[] nums) {
int ans = 0;
for (int num : nums) {
ans ^= num; // 相同数异或为 0,最终只剩出现一次的那个
}
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
169. 多数元素 (Majority Element)
数组哈希表分治计数排序简单
题目
给定大小为 n 的数组 nums,返回多数元素(出现次数大于 ⌊n/2⌋)。数组非空且一定存在多数元素。
示例 1:
输入:nums = [3,2,3]
输出:3
示例 2:
输入:nums = [2,2,1,1,1,2,2]
输出:2
提示:
1 <= n <= 5 · 10^4-10^9 <= nums[i] <= 10^9- 保证存在多数元素
进阶:O(n) 时间、O(1) 空间。
思路简述
Boyer-Moore 投票:维护候选人 candidate 与计数 cnt。相同则 cnt++,不同则 cnt--;cnt 归零时换人。因为多数元素个数过半,最后留下的一定是它:两两抵消后,多数元素无法被消完。
参考代码
JAVA
class Solution {
public int majorityElement(int[] nums) {
int candidate = nums[0]; // 当前假设的多数元素
int cnt = 0; // 候选人相对票数(遇同 +1,遇异 -1)
for (int num : nums) {
if (cnt == 0) {
candidate = num; // 票数耗尽,换当前数为新候选人
}
if (candidate == num) {
cnt++;
} else {
cnt--; // 与候选人不同:一对一抵消
}
}
int ans = candidate; // 题目保证一定存在多数元素,无需二次校验
return ans;
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
75. 颜色分类 (Sort Colors)
数组双指针排序中等
题目
给定含 0、1、2 的数组(红、白、蓝),原地排序使相同颜色相邻,且按红→白→蓝排列。不能使用库排序。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
提示:
1 <= n <= 300nums[i]为 0、1 或 2
进阶:常数空间、一趟扫描。
思路简述
一趟扫描:p0 指向下一个 0 应放的位置,p1 指向下一个 1 应放的位置。扫到 i 时先把该格写成 2(默认最右颜色),若原值 ≤ 1 再在 p1 写 1 并右移;若原值是 0,再在 p0 把刚写的 1 覆盖成 0。这样 [0, p0) 全 0,[p0, p1) 全 1,其余为 2。
参考代码
JAVA
class Solution {
public void sortColors(int[] nums) {
int p0 = 0; // 下一个 0 应写入的下标
int p1 = 0; // 下一个 1 应写入的下标
for (int i = 0; i < nums.length; i++) {
int num = nums[i];
nums[i] = 2; // 先按「最右颜色」落子,后面若是 0/1 再覆盖回来
if (num <= 1) {
nums[p1++] = 1; // 0 和 1 都先占一个 1 的坑
}
if (num == 0) {
nums[p0++] = 0; // 真正的 0 覆盖刚才写下的 1
}
}
}
}
复杂度
- 时间:O(n),一趟扫描
- 空间:O(1)
31. 下一个排列 (Next Permutation)
数组双指针中等
题目
给定整数数组 nums,原地改成字典序的下一个更大排列;若不存在,则改成升序(最小排列)。只允许常数额外空间。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
提示:
1 <= nums.length <= 1000 <= nums[i] <= 100
思路简述
从右找第一个「升序拐点」i(nums[i] < nums[i+1])。找不到说明已是最大排列,整段反转即可。否则在右侧递减后缀里找刚好比 nums[i] 大的最右元素 j,交换后把后缀反转成升序,得到 刚好大一号、且后缀最小 的下一个排列。
参考代码
JAVA
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
if (n <= 1) {
return;
}
// 从右往左找第一个升序对:nums[i] < nums[i+1],i 是要变大的位置
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i < 0) {
reverse(nums, 0, n - 1); // 整体递减:已是最大排列,翻成最小
return;
}
// 在 i 右侧(递减后缀)从右找第一个 > nums[i] 的数,保证变幅最小
int j = n - 1;
while (j > i && nums[i] >= nums[j]) {
j--;
}
swap(nums, i, j);
reverse(nums, i + 1, n - 1); // 后缀原本递减,反转后变成最小升序
}
private void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
private void reverse(int[] nums, int left, int right) {
while (left < right) {
swap(nums, left, right);
left++;
right--;
}
}
}
复杂度
- 时间:O(n)
- 空间:O(1)
287. 寻找重复数 (Find the Duplicate Number)
位运算数组双指针二分查找中等
题目
数组 nums 含 n+1 个整数,取值均在 [1, n],其中只有一个整数出现两次或多次,其余各一次。不修改数组、只用 O(1) 额外空间,返回那个重复数。
示例 1:
输入:nums = [1,3,4,2,2]
输出:2
示例 2:
输入:nums = [3,1,3,4,2]
输出:3
示例 3:
输入:nums = [3,3,3,3,3]
输出:3
提示:
1 <= n <= 10^5nums.length == n + 11 <= nums[i] <= n- 恰好一个整数重复出现
进阶:O(n) 时间。
思路简述
把下标 i 看成指向 nums[i] 的边,则形成带环链表:重复值是多个入边指向的同一结点,也就是环的入口。Floyd 判圈:快慢指针相遇后,慢指针回到起点、两者一步一步走,再次相遇处即为环入口(重复数)。
参考代码
JAVA
class Solution {
public int findDuplicate(int[] nums) {
// 将 i → nums[i] 视为链表;重复值对应环入口
int slow = 0;
int fast = 0;
while (true) {
slow = nums[slow]; // 慢指针走一步
fast = nums[nums[fast]]; // 快指针走两步
if (slow == fast) {
// 相遇后:一个从起点、一个从相遇点,同步走一步
slow = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
int ans = slow; // 再次相遇即为环入口 = 重复数
return ans;
}
}
}
}
复杂度
- 时间:O(n)
- 空间:O(1)











