78-回溯-子集

位操作 | 数组 | 回溯

题目概述

题目:给定一个整数数组 nums,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。

示例

输入:nums = [1, 2, 3]
输出:[
  [],
  [1],
  [2],
  [1,2],
  [3],
  [1,3],
  [2,3],
  [1,2,3]
]

回溯算法的基本思路

  1. 选择与不选择:对于数组中的每一个元素,我们都有两种选择:要么选择这个元素加入当前的子集中,要么不选择它。
  2. 递归探索:通过递归,我们可以探索所有可能的选择,构建出所有的子集。
  3. 回溯:当我们完成一个子集的构建后,需要撤销上一步的选择,以便尝试下一个可能的选择。

递归过程

在这里插入图片描述

我们使用一个索引 index 来代表当前考虑的元素。递归的基本步骤如下:

  1. 终止条件:当 index 超出数组的长度时,说明已经考虑完所有的元素,将当前构建的子集 list 加入结果集 res
  2. 选择当前元素:将 nums[index] 加入 list 中,然后递归调用 dfs,继续考虑下一个元素。
  3. 撤销选择:在递归返回后,将 nums[index]list 中移除,以撤销选择。
  4. 不选择当前元素:递归调用 dfs,继续考虑下一个元素,这次不将当前元素加入 list

JavaScript 实现

以下是你提供的代码,我们将逐行解析:

const subsets = (nums) => {
  const res = []; // 用于存储所有子集

  const dfs = (index, list) => {
    if (index == nums.length) { // 指针越界,说明所有数字考察完了
      res.push(list.slice());   // 将当前子集的拷贝加入解集
      return;                   // 结束当前的递归
    }
    // 选择当前元素
    list.push(nums[index]); // 将当前元素加入子集
    dfs(index + 1, list);   // 基于选择,递归考察下一个元素
    list.pop();             // 撤销选择,回到上一个状态
    // 不选择当前元素
    dfs(index + 1, list);   // 不选当前元素,继续考察下一个元素
  };

  dfs(0, []); // 从索引0开始,初始子集为空
  return res; // 返回所有子集
};

代码详解

  1. 初始化

    • res:用于存储所有可能的子集。
    • dfs:定义的递归函数,接受两个参数:index表示当前考虑的元素的索引,list表示当前构建的子集。
  2. 终止条件

    • index 等于 nums.length 时,说明已经处理完所有元素,此时将当前的 list(子集)加入到结果集 res 中。使用 list.slice() 是因为我们需要将 list 的一个拷贝添加到 res,确保后续的操作不会影响已经加入的子集。
  3. 选择与递归

    • 首先将当前元素 nums[index] 加入到 list 中,调用 dfs 继续处理下一个元素(即 index + 1)。
    • 递归结束后,使用 list.pop() 撤销选择,恢复到上一步的状态。
  4. 不选择与递归

    • 直接调用 dfs(index + 1, list),不将当前元素加入 list,继续处理下一个元素。

运行示例

考虑输入数组 nums = [1, 2, 3],我们可以跟踪函数的调用过程:

  1. 初始状态:index = 0, list = []
  2. 选择 1list = [1]
  3. 选择 2list = [1, 2]
  4. 选择 3list = [1, 2, 3]
    • 终止条件满足,添加 [1, 2, 3]res
  5. 撤销选择 3list = [1, 2]
  6. 不选择 3list = [1, 2]
    • 终止条件满足,添加 [1, 2]res
  7. 撤销选择 2list = [1]
  8. 不选择 2list = [1]
    • 继续选择 3list = [1, 3]
    • 终止条件满足,添加 [1, 3]res
  9. 撤销选择 3list = [1]
  10. 撤销选择 1list = []
  11. 不选择 1list = []
    • 选择 2list = [2]
    • 继续选择 3list = [2, 3]
    • 终止条件满足,添加 [2, 3]res
  12. 继续进行类似的步骤,最终得到所有子集。

最终结果

运行代码后,res 将包含所有的子集:

[
  [],
  [1],
  [1, 2],
  [1, 2, 3],
  [1, 3],
  [2],
  [2, 3],
  [3]
]

在这里插入图片描述
好的,让我们深入探讨你提出的另一种生成所有子集(幂集)的方法。这种方法的核心思想是通过在递归调用之前直接将当前子集加入结果集,而不是在递归结束时再添加。这使得我们可以更自然地控制递归的出口,利用循环的起点来管理可选元素。

方法概述

这种方法依然使用回溯算法,但在递归的顺序和逻辑上有所不同。我们在递归开始之前将当前的子集添加到结果集中,然后通过循环来枚举所有可选的数字。

关键思路

  1. 提前加入结果集:在每次递归调用之前,将当前构建的子集 list 加入到结果集 res
  2. 使用循环控制可选元素:通过 for 循环来枚举从当前索引 index 开始的所有后续元素,确保每次递归只考虑未被选择的元素。
  3. 递归调用:在选择了一个元素后,递归调用时的索引为 i + 1,这保证了不会重复选择已经在当前子集中包含的元素。
  4. 撤销选择:递归返回后,撤销上一步的选择,以便尝试下一个元素。

JavaScript 实现

让我们看一下你提供的代码,并逐步解析其实现逻辑:

const subsets = (nums) => {
  const res = []; // 用于存储所有子集

  const dfs = (index, list) => {
    res.push(list.slice()); // 在递归开始前,将当前子集加入结果集
    for (let i = index; i < nums.length; i++) { // 从当前索引开始,枚举所有可选的数
      list.push(nums[i]); // 选择当前元素
      dfs(i + 1, list);   // 基于选择,递归考察下一个元素
      list.pop();         // 撤销选择,回到上一个状态
    }
  };

  dfs(0, []); // 从索引0开始,初始子集为空
  return res; // 返回所有子集
};

代码详解

  1. 初始化

    • res:用于存储生成的所有子集。
    • dfs:定义的递归函数,接收两个参数:index(当前考虑的索引)和 list(当前子集)。
  2. 提前加入结果集

    • 在每次递归调用的开始,使用 res.push(list.slice()) 将当前的 list(子集)加入结果集。这里使用 slice() 是为了确保我们添加的是 list 的一个拷贝,而不是引用。
  3. 循环控制可选元素

    • 使用 for 循环,从 index 开始遍历 nums 数组的后续元素。这样可以确保每次递归都只考虑未被选择的元素。
    • for (let i = index; i < nums.length; i++) 这一行确保了不会重复选择已经在 list 中的元素。
  4. 选择与递归

    • 将当前元素 nums[i] 加入 list,然后调用 dfs(i + 1, list) 进行递归,这样下一次递归只会考虑 numsi 后面的元素。
  5. 撤销选择

    • 在递归返回后,使用 list.pop() 撤销当前选择,恢复到上一步的状态,以便尝试下一个可能的选择。

运行示例

考虑输入数组 nums = [1, 2, 3],我们可以跟踪函数的调用过程:

  1. 初始状态:index = 0, list = []
    • 加入 []res
  2. 选择 1list = [1]
    • 加入 [1]res
  3. 选择 2list = [1, 2]
    • 加入 [1, 2]res
  4. 选择 3list = [1, 2, 3]
    • 加入 [1, 2, 3]res
  5. 撤销选择 3list = [1, 2]
  6. 循环结束,撤销选择 2list = [1]
  7. 不选择 2,选择 3list = [1, 3]
    • 加入 [1, 3]res
  8. 撤销选择 3list = [1]
  9. 撤销选择 1list = []
  10. 不选择 1,选择 2list = [2]
    • 加入 [2]res
  11. 选择 3list = [2, 3]
    • 加入 [2, 3]res
  12. 撤销选择 3list = [2]
  13. 撤销选择 2list = []
  14. 不选择 12,选择 3list = [3]
    • 加入 [3]res

最终结果

运行代码后,res 将包含所有的子集:

[
  [],
  [1],
  [1, 2],
  [1, 2, 3],
  [1, 3],
  [2],
  [2, 3],
  [3]
]

总结

通过这种方法,我们利用循环和递归的结合来生成所有子集,具有以下优点:

  1. 简洁性:直接在递归开始前将当前子集加入结果集,使得逻辑更为清晰。
  2. 控制可选范围:通过循环的起始索引控制可选元素,避免了重复选择。
  3. 自然结束递归:没有显式设置递归出口,而是通过循环的控制自然结束递归。

这种方法在实现上比之前的方法稍微简洁,同时也更容易理解。通过掌握这两种方法,你可以在处理类似问题时灵活选择最合适的解法。

如果你还有其他问题或需要进一步的帮助,请随时告诉我!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值