文章目录
前言
本文记录力扣Hot100里面关于回溯的四道题,包括常见解法和一些关键步骤理解,也有例子便于大家理解
一、全排列
1.题目
给定一个不含重复数字 的数组 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] <= 10
- nums 中的所有整数 互不相同
2.代码
java
class Solution {
public List<List<Integer>> permute(int[] nums) {
// 存储最终所有排列结果
List<List<Integer>> res = new ArrayList<>();
// 临时列表,用于存放当前正在构建的排列
List<Integer> output = new ArrayList<>();
// 将数组元素加入临时列表
for (int num : nums) {
output.add(num);
}
// 启动回溯,从第 0 个位置开始填充
backtrack(nums.length, output, res, 0);
return res;
}
// 回溯函数:递归生成排列
// n:数组长度,output:当前排列,res:结果集,first:当前要固定的位置索引
private void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 递归终止条件:所有位置都已填充完成
if (first == n) {
// 将当前完整排列加入结果集(必须新建列表保存)
res.add(new ArrayList<>(output));
return;
}
// 遍历:将 [first, n-1] 的每个元素放到 first 位置
for (int i = first; i < n; i++) {
// 交换:固定 first 位置的元素
Collections.swap(output, first, i);
// 递归:处理下一个位置
backtrack(n, output, res, first + 1);
// 回溯:撤销交换,恢复原状态,继续尝试下一种可能
Collections.swap(output, first, i);
}
}
}
3.例子
以nums = [1,2,3]为例
java
// 初始
output = [1,2,3]
backtrack(n=3, output, res, first=0)
第一层:first = 0
i 从 0 开始循环:i=0,1,2
① i = 0
swap(0,0) → output 不变:[1,2,3]
进入
backtrack(3, [1,2,3], res, first=1)
第二层:first = 1
i 从 1 开始:i=1,2
① i = 1
swap(1,1) → [1,2,3]
进入
backtrack(3, [1,2,3], res, first=2)
第三层:first = 2
i 从 2 开始:i=2
i = 2
swap(2,2) → [1,2,3]
进入
backtrack(3, [1,2,3], res, first=3)
终止条件:first == n(3==3)
添加结果:
res = [[1,2,3]]
回溯:swap(2,2) 还原 → 回到上一层
回到第三层循环结束
回到第二层
② i = 2
swap(1,2) → output 变成 [1,3,2]
进入
backtrack(3, [1,3,2], res, first=2)
终止条件:first=3
添加结果:
res = [[1,2,3], [1,3,2]]
回溯:swap(1,2) 还原 → [1,2,3]
第二层循环结束
回到第一层
① i=0 结束,回溯 swap(0,0)
② i = 1
swap(0,1) → output 变成 [2,1,3]
进入
backtrack(3, [2,1,3], res, first=1)
第二层:first=1
i=1,2
i=1 → swap → [2,1,3]
进入 first=2 → 得到 [2,1,3]
加入结果:
res.add([2,1,3])
i=2 → swap(1,2) → [2,3,1]
进入 first=2 → 得到 [2,3,1]
加入结果:
res.add([2,3,1])
回溯还原 → [2,1,3] → 再回溯 → [1,2,3]
③ i = 2
swap(0,2) → output 变成 [3,2,1]
进入
backtrack(3, [3,2,1], res, first=1)
第二层:first=1
i=1,2
i=1 → [3,2,1] → 加入结果
i=2 → swap → [3,1,2] → 加入结果
最终结果
[[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,2,1],
[3,1,2]]
非常清晰了吧!
二、子集
1.题目
给你一个整数数组 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] <= 10
- nums 中的所有元素 互不相同
2.代码
java
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>(); // 存储所有子集的结果集
List<Integer> temp = new ArrayList<>(); // 存储当前正在构建的子集
dfs(nums, 0, temp, res); // 从第0个元素开始递归
return res;
}
// pathLen 是当前递归处理到数组nums的第几个元素(即元素下标),也可以理解为递归的深度
private void dfs(int[] nums, int pathLen, List<Integer> temp, List<List<Integer>> res) {
// 终止条件:处理完所有元素(pathLen等于数组长度)
if (pathLen == nums.length) {
res.add(new ArrayList<>(temp)); // 将当前子集的副本加入结果集
return;
}
// 分支1:选择当前元素(将nums[pathLen]加入当前子集)
temp.add(nums[pathLen]);
dfs(nums, pathLen + 1, temp, res); // 递归处理下一个元素
temp.remove(temp.size() - 1); // 回溯:撤销选择
// 分支2:不选择当前元素(直接处理下一个元素)
dfs(nums, pathLen + 1, temp, res);
}
}
关于dfs函数的执行过程
1. 先判断是否到底了,到底就保存子集
2. 先走"选"的分支
加进去 → 递归 → 撤销(删掉)
3. 再走"不选"的分支
直接递归
3.例子
以nums = [1,2,3]为例
初始状态
temp = []
pathLen = 0(处理数字 1)
开始递归!
1️⃣ 处理 1(pathLen=0)
分支1:选 1
temp 变成 [1]
进入下一层 → pathLen=1(处理 2)
2️⃣ 处理 2(pathLen=1)
分支1:选 2
temp 变成 [1,2]
进入下一层 → pathLen=2(处理 3)
3️⃣ 处理 3(pathLen=2)
分支1:选 3
temp 变成 [1,2,3]
进入下一层 → pathLen=3
4️⃣ pathLen=3(等于数组长度,终止)
把 [1,2,3] 加入结果!
返回!
回到 3️⃣,执行回溯:撤销选择
temp 删除最后一个元素
1,2,3\] → \[1,2
分支2:不选 3
直接递归 → pathLen=3
把 [1,2] 加入结果!
返回!
回到 2️⃣,执行回溯:撤销选择
temp 删除最后一个元素
1,2\] → \[1
分支2:不选 2
直接递归 → pathLen=2(处理 3)
5️⃣ 处理 3(pathLen=2)
分支1:选 3
temp → [1,3]
加入结果!
回溯撤销 → [1]
分支2:不选 3
temp → [1]
加入结果!
回到 1️⃣,执行回溯:撤销选择
temp 删除最后一个元素
1\] → \[
分支2:不选 1
直接递归 → pathLen=1(处理 2)
6️⃣ 处理 2(pathLen=1)
分支1:选 2
temp → [2]
处理 3:
选 3 → [2,3](加入结果)
不选 3 → [2](加入结果)
回溯撤销 → []
分支2:不选 2
直接处理 3:
选 3 → [3](加入结果)
不选 3 → [](加入结果)
最终所有子集
1,2,3
1,2
1,3
1
2,3
2
3
三、电话号码的字母组合
1.题目
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = "2"
输出:["a","b","c"]
提示:
- 1 <= digits.length <= 4
- digits[i] 是范围 ['2', '9'] 的一个数字。
2.代码
java
class Solution {
// 存储最终的字母组合结果集
List<String> res = new ArrayList<>();
// 临时存储当前构建的字母组合字符串
StringBuilder sb = new StringBuilder();
public List<String> letterCombinations(String digits) {
// 边界条件:若输入数字字符串为空,直接返回空结果集
if (digits.isEmpty()) {
return res;
}
// 构建数字到字母的映射表,对应电话键盘的布局
Map<Character, String> map = new HashMap<>();
map.put('2', "abc");
map.put('3', "def");
map.put('4', "ghi");
map.put('5', "jkl");
map.put('6', "mno");
map.put('7', "pqrs");
map.put('8', "tuv");
map.put('9', "wxyz");
// 从数字字符串的第0个位置开始递归遍历构建组合
traverseTree(digits, 0, map);
return res;
}
public void traverseTree(String digits, int index, Map<Character, String> map) {
// 终止条件:当前构建的字符串长度等于数字字符串长度,说明已完成一个组合
if (digits.length() == sb.length()) {
res.add(sb.toString()); // 将当前组合加入结果集
return; // 结束当前递归分支,返回上一层
}
// 获取当前索引对应的数字字符,并得到其映射的字母字符串
String str = map.get(digits.charAt(index));
// 遍历当前数字映射的每个字母,尝试加入组合
for (int i = 0; i < str.length(); i++) {
sb.append(str.charAt(i)); // 选择当前字母,加入临时字符串
traverseTree(digits, index + 1, map); // 递归处理下一个数字字符
sb.deleteCharAt(sb.length() - 1); // 回溯:撤销选择,删除最后一个字母
}
}
}
1. 为什么不能直接用 String?
因为 Java 中 String 是不可变的!每次 String s = s + "a" 都会创建一个新的字符串对象,非常浪费内存、速度慢。
回溯算法需要大量拼接、删除字符,用 String 会:
- 产生大量垃圾对象
- 效率极低
- 代码麻烦(每次都要新建)
2. 为什么要用 StringBuilder?
它是可变字符串,专门用来:
- 高效拼接
- 高效删除
- 原地修改,不产生新对象
回溯里这两步必须靠它:
java
sb.append(字符); // 加字符(选择)
sb.deleteCharAt(); // 删最后一个(回溯撤销)
3. StringBuilder vs StringBuffer 区别
- StringBuilder:非线程安全,速度更快 → 刷题首选
- StringBuffer:线程安全,速度慢一点 → 多线程
3.例子
用 digits = "23" 为例子
输入:23 → 数字 2 → abc,数字 3 → def
初始状态
res = []
sb = 空
开始执行 traverseTree("23", 0, map)
第1层递归:index = 0(处理数字 '2' → "abc")
sb 长度 0 ≠ 2 → 继续
str = "abc"
进入循环 i = 0, 1, 2
i = 0,字符 'a'
- sb.append('a') → sb = "a"
- 递归进入
traverseTree("23", 1, map)
第2层递归:index = 1(处理数字 '3' → "def")
sb 长度 1 ≠ 2 → 继续
str = "def"
进入循环 i = 0,1,2
i=0,字符 'd'
- sb.append('d') → sb = "ad"
- 递归进入
traverseTree("23", 2, map)
第3层递归:index=2
sb 长度 2 == 2
res.add("ad") → res = ["ad"]
返回
- sb.deleteCharAt → sb = "a"
i=1,字符 'e'
- sb.append('e') → sb = "ae"
- 递归 → index=2 → 加入结果
res = ["ad","ae"] - 删除 → sb = "a"
i=2,字符 'f'
- sb.append('f') → sb = "af"
- 递归 → 加入结果
res = ["ad","ae","af"] - 删除 → sb = "a"
回到第2层循环结束
回到第1层
第1层 i=0 结束:
sb.deleteCharAt → sb = ""
第1层 i=1,字符 'b'
- sb.append('b') → sb = "b"
- 递归 index=1(处理3)
循环 d/e/f
得到:
"bd", "be", "bf"
加入 res - 删除 → sb = ""
第1层 i=2,字符 'c'
- sb.append('c') → sb = "c"
- 递归 index=1(处理3)
循环 d/e/f
得到:
"cd","ce","cf"
加入 res - 删除 → sb = ""
最终结果
["ad","ae","af","bd","be","bf","cd","ce","cf"]
四、组合总和
1.题目
给你一个 无重复元素 的整数数组 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 <= 30
- 2 <= candidates[i] <= 40
- candidates 的所有元素 互不相同
- 1 <= target <= 40
2.代码
java
class Solution {
// 主函数
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> combine = new ArrayList<>();
dfs(candidates, target, ans, combine, 0);
return ans;
}
// 回溯DFS
public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> combine, int idx) {
// 遍历完数组,返回
if (idx == candidates.length) return;
// 找到有效组合,加入结果
if (target == 0) {
ans.add(new ArrayList<>(combine));
return;
}
// 不选当前数
dfs(candidates, target, ans, combine, idx + 1);
// 选当前数
if (target >= candidates[idx]) {
combine.add(candidates[idx]);
dfs(candidates, target - candidates[idx], ans, combine, idx);
combine.remove(combine.size() - 1); // 回溯
}
}
}
3.例子
用 candidates = [2,3,5],target = 8 为例
输入:[2,3,5], target=8
代码执行顺序:先不选 → 再选
初始状态
ans = []
combine = []
调用:dfs(8, [], 0)
第1层:idx=0,数字=2,target=8
- 没到头,target≠0
- 先走:不选 2
→ 进入 dfs(8, [], 1)
第2层:idx=1,数字=3,target=8
- 没到头,target≠0
- 先走:不选 3
→ 进入 dfs(8, [], 2)
第3层:idx=2,数字=5,target=8
-
没到头,target≠0
-
先走:不选 5
→ 进入 dfs(8, [], 3)
→ idx=3 越界,返回
-
选 5 :8≥5
combine.add(5) → [5]
进入 dfs(3, [5], 2)
- 不选5 → 返回
- 3<5 → 不能选
返回
combine.remove → []
回到第2层
回到第2层:idx=1,数字=3,target=8
- 选 3 :8≥3
combine.add(3) → [3]
进入 dfs(5, [3], 1)
第3层:idx=1,数字=3,target=5
- 不选 3 → dfs(5, [3], 2)
第4层:idx=2,数字=5,target=5
-
不选5 → 返回
-
选5 :5≥5
combine.add(5) → [3,5]
进入 dfs(0, [3,5], 2)
✅ target=0
ans.add([3,5])
ans = [[3,5]]回溯删除 → [3]
返回
回到第3层
- 选3:5≥3 → combine=[3,3]
进入 dfs(2, [3,3],1)
2<3,不选,返回
回溯删除 → [3]
返回第2层
回溯删除 → []
回到第1层:idx=0,数字=2,target=8
- 选 2 :8≥2
combine.add(2) → [2]
进入 dfs(6, [2], 0)
第2层:idx=0,数字=2,target=6
- 不选 2 → dfs(6, [2], 1)
第3层:idx=1,数字=3,target=6
- 不选3 → 无结果
- 选3 :6≥3
combine.add(3) → [2,3]
进入 dfs(3, [2,3], 1)
第4层:idx=1,数字=3,target=3
- 不选3 → 无结果
- 选3 :3≥3
combine.add(3) → [2,3,3]
进入 dfs(0, [2,3,3],1)
✅ target=0
ans.add([2,3,3])
ans = [[3,5], [2,3,3]]
回溯删除 → [2,3] → [2]
回到第2层:idx=0,数字=2,target=6
- 选 2 :6≥2
combine.add(2) → [2,2]
进入 dfs(4, [2,2], 0)
第3层:idx=0,数字=2,target=4
选2 → [2,2,2]
进入 dfs(2, [2,2,2], 0)
第4层:idx=0,数字=2,target=2
选2 → [2,2,2,2]
进入 dfs(0, ...)
✅ target=0
ans.add([2,2,2,2])
ans = [[3,5], [2,3,3], [2,2,2,2]]
最终结果:
[[3,5], [2,3,3], [2,2,2,2]]
如果本篇文章对您有帮助,可以点赞,收藏或评论哦!!!关注主包不迷路,让我们一起向前进步吧!!