本系列为笔者的 Leetcode 刷题记录,顺序为 Hot 100 题官方顺序,根据标签命名,记录笔者总结的做题思路,附部分代码解释和疑问解答,01~07为C++语言,08及以后为Java语言。
01 全排列

class Solution {
public List<List<Integer>> permute(int[] nums) {
}
}
预备知识
回溯法:一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认不是一个解(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变化抛弃该解,即回溯并且再次尝试。

class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> output = new ArrayList<>();
for(int num : nums){
output.add(num);
}
int n = nums.length;
myFunction(n, result, output, 0);
return result;
}
/**
* 回溯方法,递归生成全排列
* @param n 数组长度
* @param output 当前排列的列表状态
* @param res 存储所有排列的结果列表
* @param first 当前固定元素的位置(从0开始)
*/
public void myFunction(int n, List<List<Integer>> result, List<Integer> output, int first){
if(first == n){
result.add(new ArrayList<>(output));
return;
}
for(int i=first; i<n; i++){
Collections.swap(output, first, i);
myFunction(n, result, output, first + 1);
Collections.swap(output, first, i);
}
}
}
① Collections是啥?
Collections 是 Java 标准库中 java.util 包下的一个工具类,提供了一系列静态方法,用来操作或返回集合(Collection)类型的对象,比如 List、Set 等。
常用功能包括:
- 排序:
Collections.sort(List<T> list)可以对列表进行排序 - 交换元素:
Collections.swap(List<?> list, int i, int j)用于交换列表中指定位置的两个元素 - 查找:
Collections.max()、Collections.min()找最大值、最小值 - 填充、复制、反转:
fill()、copy()、reverse()等
02 子集

class Solution {
public List<List<Integer>> subsets(int[] nums) {
}
}
方法一:神奇二进制
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> output = new ArrayList<>();
int n = nums.length;
for(int mask=0; mask<(1<<n); mask++){ //遍历0~2^n-1之间的数
output.clear();
for(int i=0; i<n; i++){ //遍历0~n-1之间的位
if((mask & (1<<i)) != 0){
output.add(nums[i]);
}
}
result.add(new ArrayList<>(output));
}
return result;
}
}
方法二:深度优先搜索
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> output = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
myFunction(0, nums);
return result;
}
public void myFunction(int curr, int[] nums){
if(curr == nums.length){
result.add(new ArrayList<>(output));
return;
}
output.add(nums[curr]);
myFunction(curr+1, nums);
output.remove(output.size()-1);
myFunction(curr+1, nums);
}
}
① t.add(nums[cur]);是头添加还是尾添加?t.remove(t.size() - 1);是头删除还是尾删除?
t.add(nums[cur]); 是尾部添加,而 t.remove(t.size() - 1); 是尾部删除。
② List<List<Integer>> result = new ArrayList<>();默认访问权限是啥?public还是protected还是private?
变量声明时如果不写访问修饰符,默认就是“包访问权限”(package-private),即默认访问权限。
③ output.remove(output.size()-1);为啥是output.size()-1而不是nums[curr]?
output.size() - 1是列表中最后一个元素的索引。output.remove(index)是根据索引删除指定位置的元素。
为什么一般不用 output.remove(nums[curr])?
考虑场景:
nums = [1, 2, 2]
递归过程中,output 可能是 [1, 2, 2]。
- 当想撤销回溯,删除最后一个
2时,如果用output.remove(nums[curr]),会删除列表中第一个2,而不是刚刚加进去的那个尾部元素。 - 导致撤销操作不正确,破坏递归的状态维护。
03 电话号码的字母组合


class Solution {
public List<String> letterCombinations(String digits) {
//1.创建 List<String> 和 Map<Character, String>
List<String> combinations = new ArrayList<>();
if(digits.length() == 0){
return combinations;
}
Map<Character, String> phoneMap = new HashMap<>(){{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
myFunction(combinations, phoneMap, digits, 0, new StringBuffer());
return combinations;
}
public void myFunction(List<String> combinations, Map<Character, String> phoneMap,
String digits, int index, StringBuffer combination){
//2.核心操作
if(index == digits.length()){
combinations.add(combination.toString());
}else{
//3.递归(回溯)
char digit = digits.charAt(index); //'2'
String letters = phoneMap.get(digit); //"abc"
int count = letters.length(); //3
for(int i=0; i<count; i++){
combination.append(letters.charAt(i));
myFunction(combinations, phoneMap, digits, index+1, combination);
combination.deleteCharAt(index); //⭐
}
}
}
}
① new StringBuffer()啥意思?
new StringBuffer()表示创建一个空的、可以修改的字符串缓冲区对象。
在回溯算法中,利用StringBuffer可以高效地构建和修改当前的字符串组合,比如append添加字符,deleteCharAt删除字符,实现回溯过程中的“选择”和“撤销选择”。
② Map<Character, String> phoneMap为啥要加双大括号,并写一堆put?
内层的{{ ... }}是一个匿名内部类的实例初始化块。
具体来说:
- 第一对大括号
{}是匿名内部类的定义体 - 第二对大括号
{}是匿名内部类的实例初始化块,放构造过程中要执行的代码
③ combination.toString()啥意思?
combination.toString() 的意思是将 combination 这个对象转换成一个普通的 String 类型,这一步是必要的,因为 combinations 是 List<String>,需要的是不可变的字符串。
04 组合总和


自己写的:
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> output = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//1.创建
/*
List<List<Integer>> result = new ArrayList<>();
List<Integer> output = new ArrayList<>();
*/
myFunction(0, 0, target, candidates);
return result;
//2.核心步骤
/*
if(sum == target){
result.add(new ArrayList<>(output));
}
*/
//3.递归(回溯)
/*
myFunction(int curr, int sum, int target, int[] candidates); //当前数组,当前位置
*/
}
public void myFunction(int curr, int sum, int target, int[] candidates){
//特殊情况判断①
if(curr == candidates.length){
return;
}
//特殊情况判断②
if(sum == target){
result.add(new ArrayList<>(output));
return;
}
if(sum + candidates[curr] <= target){
output.add(candidates[curr]);
myFunction(curr, sum + candidates[curr], target, candidates);
output.remove(output.size() - 1);
}
myFunction(curr + 1, sum, target, candidates);
}
}
05 括号总和

class Solution {
List<String> result = new ArrayList<>();
public List<String> generateParenthesis(int n) {
//1.创建
/*
List<String> result = new ArrayList<>();
StringBuffer output
*/
myFunction(n, 0, 0, new StringBuffer());
return result;
//2.核心操作
/*
if(output.length() == n * 2){
result.add(output.toString());
return;
}
*/
//3.递归(回溯)
/*
n: 括号对数
indexLeft: 左括号当前下标
indexRight: 右括号当前下标
output: 可变长度 String 字符串
myFunction(int n, int indexLeft, int indexRight, StringBuffer output)
*/
}
public void myFunction(int n, int indexLeft, int indexRight, StringBuffer output){
if(output.length() == n * 2){
result.add(output.toString());
return;
}
if(indexLeft < n){
output.append("(");
myFunction(n, indexLeft + 1, indexRight, output);
output.deleteCharAt(output.length() - 1);
}
if(indexLeft > indexRight){
output.append(")");
myFunction(n, indexLeft, indexRight + 1, output);
output.deleteCharAt(output.length() - 1);
}
}
}
06 单词搜索




自己写的:
class Solution {
boolean flag = false;
public boolean exist(char[][] board, String word) {
//1.创建
/*
boolean flag
StringBuffer output
*/
myFunction(0, 0, board, word, new StringBuffer());
return flag;
//2.核心操作
/*
if(output == word){
flag = true;
return;
}
*/
//3.递归(回溯)
/*
myFunction(int row, int column, char[][] board, String word, StringBuffer output)
*/
}
public void myFunction(int row, int column, char[][] board, String word, StringBuffer output){
if(output == word){
flag = true;
return;
}
output.append(board[row][column]);
if(row+1 < board.length){
myFunction(row+1, column, board, word);
}
if(column+1 < board[0].length){
myFunction(row, column+1, board, word);
}
output.deleteCharAt(output.size()-1);
}
}
参考答案:
class Solution {
public boolean exist(char[][] board, String word) {
int h = board.length, w = board[0].length;
boolean[][] visited = new boolean[h][w];
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
boolean flag = check(board, visited, i, j, word, 0);
if (flag) {
return true;
}
}
}
return false;
}
public boolean check(char[][] board, boolean[][] visited, int i, int j, String s, int k) {
if (board[i][j] != s.charAt(k)) {
return false;
} else if (k == s.length() - 1) {
return true;
}
visited[i][j] = true;
int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
boolean result = false;
for (int[] dir : directions) {
int newi = i + dir[0], newj = j + dir[1];
if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) {
if (!visited[newi][newj]) {
boolean flag = check(board, visited, newi, newj, s, k + 1);
if (flag) {
result = true;
break;
}
}
}
}
visited[i][j] = false;
return result;
}
}
07 分割回文串

class Solution {
public List<List<String>> partition(String s) {
}
}
① Arrays.fill(f[i], true); 是什么意思?
f是一个二维布尔数组,f[i]表示二维数组f的第i行(也是一个布尔数组)。Arrays.fill是Java标准库中用于快速给数组填充值的方法。Arrays.fill(f[i], true);的意思是:将数组f[i]中的每个元素,都赋值为true。
② 为什么这样遍历?
for (int i = n - 1; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];
}
}
动态规划的状态转移是:
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i+1][j-1];
也就是说:
子串[i, j]是否是回文,取决于:
s[i]和s[j]是否相等- 中间的子串
[i+1, j-1]是否是回文(即f[i+1][j-1])
因此,需要先保证f[i+1][j-1]已经被计算出来,才能正确计算f[i][j]。
假如从前往后遍历 i,那f[i+1][j-1]还没有计算,dp就没法用。
③ ans.add(s.substring(i, j + 1));为什么是j+1而不是j?
这是因为Java中String的substring方法的语法是:
s.substring(beginIndex, endIndex)
beginIndex是子串起始索引(包含)。endIndex是子串结束索引(不包含)。
所以substring(i, j + 1)表示取得字符串从索引i开始,到j结束(包含位置j的字符),刚好是你想要的子串s[i..j]。
④ ret.add(new ArrayList<>(ans)); 是什么是“深拷贝”?
- 因为
ans是递归过程中不断修改的同一个对象,它会随着回溯加入和删除元素。 - 如果不复制,只把
ans本身加入结果,后续修改会导致结果集中所有引用都变成最后的状态,结果错误。 - 复制一份保证当前这个状态的路径是独立的,不会被后续递归修改。
什么是深拷贝?
- 浅拷贝只复制引用,多个对象共享同一份内存数据(比如指向同一个列表),修改其中一个会影响所有。
- 深拷贝则复制对象及其内部包含的数据,生成完全独立的对象。
⑤ dfs(s, j + 1);为什么新的递归起始位置是 j + 1?
- 当前选择的回文子串是
s[i..j](包含i到j)。 - 下一步需要找的是紧接着当前子串后面的部分,即以
j + 1为起点的剩余字符串。 - 因为回文分割要求子串连续且不重叠,每找到一个回文子串后,搜索区间从它的下一个位置开始。
class Solution {
//1.创建
boolean[][] f;
List<List<String>> result = new ArrayList<>();
List<String> output = new ArrayList<>();
int n;
public List<List<String>> partition(String s) {
n = s.length();
f = new boolean[n][n];
for(int i=0; i<n; i++){
Arrays.fill(f[i], true);
}
for(int i=n-1; i>=0; --i){
for(int j=i+1; j<n; j++){
f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i+1][j-1];
}
}
myFunction(s, 0);
return result;
}
public void myFunction(String s, int i){
//2.核心操作
if(i == s.length()){
result.add(new ArrayList<>(output));
return;
}
//3.递归(回溯)
for(int j=i; j<n; ++j){
if(f[i][j]){
output.add(s.substring(i, j+1));
myFunction(s, j+1);
output.remove(output.size()-1);
}
}
}
}
08 N 皇后


方法一:基于集合的回溯
class Solution {
public List<List<String>> solveNQueens(int n) {
//1.创建
List<List<String>> result = new ArrayList<>();
int[] queens = new int[n];
Arrays.fill(queens, -1);
Set<Integer> columns = new HashSet<>(); //列
Set<Integer> diagonals1 = new HashSet<>(); //主对角线 row - i
Set<Integer> diagonals2 = new HashSet<>(); //副对角线 row + i
myFunction(result, queens, n, 0, columns, diagonals1, diagonals2);
return result;
}
public void myFunction(List<List<String>> result, int[] queens, int n, int row,
Set<Integer> columns, Set<Integer> diagonals1, Set<Integer> diagonals2){
//2.核心操作
if(n == row){
List<String> output = myFunction2(queens, n);
result.add(output);
}else{
//遍历全列,寻找插入皇后的位置
for(int i=0; i<n; i++){
if(columns.contains(i)){
continue;
}
int diagonal1 = row - i;
if(diagonals1.contains(diagonal1)){
continue;
}
int diagonal2 = row + i;
if(diagonals2.contains(diagonal2)){
continue;
}
//3.递归(回溯)
queens[row] = i;
columns.add(i);
diagonals1.add(diagonal1);
diagonals2.add(diagonal2);
myFunction(result, queens, n, row+1, columns, diagonals1, diagonals2);
queens[row] = -1;
columns.remove(i);
diagonals1.remove(diagonal1);
diagonals2.remove(diagonal2);
}
}
}
public List<String> myFunction2(int[] queens, int n){
List<String> output = new ArrayList<>();
for(int i=0; i<n; i++){
char[] row = new char[n];
Arrays.fill(row, '.');
row[queens[i]] = 'Q';
output.add(new String(row));
}
return output;
}
}
方法二:基于位运算的回溯
① 为什么 x & (-x) 可以获得 x 的二进制表示中的最低位的 1 的位置?
x是一个二进制数,如 01011000。-x是x的补码表示,计算方式是对x取反后加 1,-x = ~x + 1,二进制中最低的那个 1 保持位置不变,之后的位变成了 0。
② 为什么 x & (x - 1) 可以将 x 的二进制表示中的最低位的 1 置成 0?
举例:
假设 x = 01011000,最低位的 1 是倒数第 4 位。
x= 01011000x - 1= 01010111(减 1 会把最低位的 1 减为 0,后面的 0 变成 1)
然后计算:
x = 01011000
x-1 = 01010111
x & (x - 1) = 01010000
可以看到,最低位的 1(第 4 位)被成功清除。
③ int availablePositions = ((1 << n) - 1) & (~(columns | diagonals1 | diagonals2));啥意思?
(1 << n) - 1:相当于让低n位变成1,高位是0。比如n=4时,(1 << 4) - 1 = 15,二进制是1111,生成一个低n位为1的掩码,限制后续的位运算结果不超过n位范围。(columns | diagonals1 | diagonals2):这是把3个已有占用位置的状态进行“或运算”,得到所有被占用的位置集合。~(columns | diagonals1 | diagonals2):取反操作,表示所有没有被占用的位置,即可用位置。&与前面((1 << n) - 1)进行“与运算”,保证结果只在低n位,防止高位的误差。
④ int column = Integer.bitCount(position - 1);啥意思?
position是一个位掩码,且只有1个二进制位为1(我们通过availablePositions & (-availablePositions)拿到的最低位1),比如position = 00001000。position - 1则是将最低的那个1位变成0,且该位右边的位全部置1,比如:
position = 00001000(第4位是1,代表第3列)
position - 1 = 00000111Integer.bitCount(x)是Java内置函数,计算x的二进制表示中有多少个1。
举例:
position = 00001000 (二进制),position - 1 = 00000111 (二进制),bitCount(7) = 3,说明皇后要放在第3列(从0开始计数)。
⑤ columns | position 中的|啥意思?
它会对两个整数的二进制每一位进行“或”操作,只要对应位上有1,结果位就是1, 否则是0。
举例:
| 列(从右到左) | 位3 | 位2 | 位1 | 位0 |
|---|---|---|---|---|
| columns | 0 | 0 | 1 | 0 |
| position | 0 | 1 | 0 | 0 |
| columns | position | 0 | 1 | 1 | 0 |
1312

被折叠的 条评论
为什么被折叠?



