全排列 vs 子集:一篇搞懂两类典型回溯问题

100 阅读2分钟

🎯 一、题意区别

类型问题描述
子集给定数组 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; i < nums.length; i++) {
      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^nO(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!