🎯 一、题意区别
| 类型 | 问题描述 |
|---|
| 子集 | 给定数组 nums,找出所有元素组合(不考虑顺序、不重复) |
| 全排列 | 给定数组 nums,找出所有可能的排列顺序(每个数都用一次) |
例如,输入 [1, 2]:
- 子集输出:
[[], [1], [2], [1,2]]
- 排列输出:
[[1,2], [2,1]]
🔍 二、回溯框架模板
✅ 子集问题(组合)
js
复制编辑
const subsets = function(nums) {
const res = []
const dfs = (index, path) => {
res.push([...path])
for (let i = index; i < nums.length; i++) {
path.push(nums[i])
dfs(i + 1, path)
path.pop()
}
}
dfs(0, [])
return res
}
- ✅ 从当前 index 开始选择,避免重复
- ✅ 每走一步都加入 res,表示一个子集
- ✅ 一般结果数量是
2^n
✅ 全排列问题(排列)
js
复制编辑
const permute = function(nums) {
const res = []
const used = new Array(nums.length).fill(false)
const dfs = (path) => {
if (path.length === nums.length) {
res.push([...path])
return
}
for (let i = 0
if (used[i]) continue
used[i] = true
path.push(nums[i])
dfs(path)
path.pop()
used[i] = false
}
}
dfs([])
return res
}
- ✅ 使用
used 标记已经选择的元素
- ✅ 每次选择一个未使用的元素加入 path
- ✅ 回溯时撤销使用状态
- ✅ 结果数量是
n!
🤔 三、常见误区对比
| 易错点 | 子集问题 | 全排列问题 |
|---|
是否使用 used 数组 | ❌ 不需要,只看起始 index | ✅ 必须使用,防止重复选同一个元素 |
| 每层是否需要判断重复元素 | ✅ 如果数组有重复,需先排序 + 去重处理 | ✅ 同样需排序 + used[i - 1] 判断 |
| 结果是否提前加入 | ✅ 每一步都可以 push 到结果(空集也算) | ❌ 只在构建满一组排列时才加入结果 |
| 是否控制起始位置 index | ✅ 必须用 index 保证组合不重复 | ❌ 不需要,任何未使用元素都可以尝试 |
| 是否顺序无关 | ✅ [1,2] 和 [2,1] 视为一个子集 | ❌ [1,2] 和 [2,1] 是不同排列 |
🧠 四、时间复杂度
| 类型 | 组合数 | 时间复杂度 |
|---|
| 子集 | 2^n | O(2ⁿ·n) |
| 全排列 | n! | O(n!·n) |
🛠 五、进阶变种
| 题目类别 | 示例题目 | 特点 |
|---|
| 子集(含重复) | LeetCode 90. 子集 II | 要排序 + 剪枝 if (i > index && nums[i] === nums[i - 1]) continue |
| 全排列(含重复) | LeetCode 47. 全排列 II | 要排序 + used[i - 1] 剪枝 |
| k 个数的组合 | LeetCode 77. 组合 | 限制组合大小为 k |
| 组合总和问题 | LeetCode 39/40. 组合总和 I/II | 加上剪枝和 target 剪枝 |
📌 六、总结一张图记住
text
复制编辑
子集(Subsets):
✅ 去重:index 控制
✅ 不需要 used[]
✅ 每一层都 push 结果
✅ 结果数:2^n
全排列(Permutation):
✅ used 控制选过的数
✅ 只 push 满长度结果
✅ 每一层从 0 遍历
✅ 结果数:n!