位操作 | 数组 | 回溯
题目概述
题目:给定一个整数数组 nums
,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。
示例:
输入:nums = [1, 2, 3]
输出:[
[],
[1],
[2],
[1,2],
[3],
[1,3],
[2,3],
[1,2,3]
]
回溯算法的基本思路
- 选择与不选择:对于数组中的每一个元素,我们都有两种选择:要么选择这个元素加入当前的子集中,要么不选择它。
- 递归探索:通过递归,我们可以探索所有可能的选择,构建出所有的子集。
- 回溯:当我们完成一个子集的构建后,需要撤销上一步的选择,以便尝试下一个可能的选择。
递归过程
我们使用一个索引 index
来代表当前考虑的元素。递归的基本步骤如下:
- 终止条件:当
index
超出数组的长度时,说明已经考虑完所有的元素,将当前构建的子集list
加入结果集res
。 - 选择当前元素:将
nums[index]
加入list
中,然后递归调用dfs
,继续考虑下一个元素。 - 撤销选择:在递归返回后,将
nums[index]
从list
中移除,以撤销选择。 - 不选择当前元素:递归调用
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; // 返回所有子集
};
代码详解
-
初始化:
res
:用于存储所有可能的子集。dfs
:定义的递归函数,接受两个参数:index
表示当前考虑的元素的索引,list
表示当前构建的子集。
-
终止条件:
- 当
index
等于nums.length
时,说明已经处理完所有元素,此时将当前的list
(子集)加入到结果集res
中。使用list.slice()
是因为我们需要将list
的一个拷贝添加到res
,确保后续的操作不会影响已经加入的子集。
- 当
-
选择与递归:
- 首先将当前元素
nums[index]
加入到list
中,调用dfs
继续处理下一个元素(即index + 1
)。 - 递归结束后,使用
list.pop()
撤销选择,恢复到上一步的状态。
- 首先将当前元素
-
不选择与递归:
- 直接调用
dfs(index + 1, list)
,不将当前元素加入list
,继续处理下一个元素。
- 直接调用
运行示例
考虑输入数组 nums = [1, 2, 3]
,我们可以跟踪函数的调用过程:
- 初始状态:
index = 0
,list = []
- 选择
1
,list = [1]
- 选择
2
,list = [1, 2]
- 选择
3
,list = [1, 2, 3]
- 终止条件满足,添加
[1, 2, 3]
到res
- 终止条件满足,添加
- 撤销选择
3
,list = [1, 2]
- 不选择
3
,list = [1, 2]
- 终止条件满足,添加
[1, 2]
到res
- 终止条件满足,添加
- 撤销选择
2
,list = [1]
- 不选择
2
,list = [1]
- 继续选择
3
,list = [1, 3]
- 终止条件满足,添加
[1, 3]
到res
- 继续选择
- 撤销选择
3
,list = [1]
- 撤销选择
1
,list = []
- 不选择
1
,list = []
- 选择
2
,list = [2]
- 继续选择
3
,list = [2, 3]
- 终止条件满足,添加
[2, 3]
到res
- 选择
- 继续进行类似的步骤,最终得到所有子集。
最终结果
运行代码后,res
将包含所有的子集:
[
[],
[1],
[1, 2],
[1, 2, 3],
[1, 3],
[2],
[2, 3],
[3]
]
好的,让我们深入探讨你提出的另一种生成所有子集(幂集)的方法。这种方法的核心思想是通过在递归调用之前直接将当前子集加入结果集,而不是在递归结束时再添加。这使得我们可以更自然地控制递归的出口,利用循环的起点来管理可选元素。
方法概述
这种方法依然使用回溯算法,但在递归的顺序和逻辑上有所不同。我们在递归开始之前将当前的子集添加到结果集中,然后通过循环来枚举所有可选的数字。
关键思路
- 提前加入结果集:在每次递归调用之前,将当前构建的子集
list
加入到结果集res
。 - 使用循环控制可选元素:通过
for
循环来枚举从当前索引index
开始的所有后续元素,确保每次递归只考虑未被选择的元素。 - 递归调用:在选择了一个元素后,递归调用时的索引为
i + 1
,这保证了不会重复选择已经在当前子集中包含的元素。 - 撤销选择:递归返回后,撤销上一步的选择,以便尝试下一个元素。
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; // 返回所有子集
};
代码详解
-
初始化:
res
:用于存储生成的所有子集。dfs
:定义的递归函数,接收两个参数:index
(当前考虑的索引)和list
(当前子集)。
-
提前加入结果集:
- 在每次递归调用的开始,使用
res.push(list.slice())
将当前的list
(子集)加入结果集。这里使用slice()
是为了确保我们添加的是list
的一个拷贝,而不是引用。
- 在每次递归调用的开始,使用
-
循环控制可选元素:
- 使用
for
循环,从index
开始遍历nums
数组的后续元素。这样可以确保每次递归都只考虑未被选择的元素。 for (let i = index; i < nums.length; i++)
这一行确保了不会重复选择已经在list
中的元素。
- 使用
-
选择与递归:
- 将当前元素
nums[i]
加入list
,然后调用dfs(i + 1, list)
进行递归,这样下一次递归只会考虑nums
中i
后面的元素。
- 将当前元素
-
撤销选择:
- 在递归返回后,使用
list.pop()
撤销当前选择,恢复到上一步的状态,以便尝试下一个可能的选择。
- 在递归返回后,使用
运行示例
考虑输入数组 nums = [1, 2, 3]
,我们可以跟踪函数的调用过程:
- 初始状态:
index = 0
,list = []
- 加入
[]
到res
- 加入
- 选择
1
,list = [1]
- 加入
[1]
到res
- 加入
- 选择
2
,list = [1, 2]
- 加入
[1, 2]
到res
- 加入
- 选择
3
,list = [1, 2, 3]
- 加入
[1, 2, 3]
到res
- 加入
- 撤销选择
3
,list = [1, 2]
- 循环结束,撤销选择
2
,list = [1]
- 不选择
2
,选择3
,list = [1, 3]
- 加入
[1, 3]
到res
- 加入
- 撤销选择
3
,list = [1]
- 撤销选择
1
,list = []
- 不选择
1
,选择2
,list = [2]
- 加入
[2]
到res
- 加入
- 选择
3
,list = [2, 3]
- 加入
[2, 3]
到res
- 加入
- 撤销选择
3
,list = [2]
- 撤销选择
2
,list = []
- 不选择
1
、2
,选择3
,list = [3]
- 加入
[3]
到res
- 加入
最终结果
运行代码后,res
将包含所有的子集:
[
[],
[1],
[1, 2],
[1, 2, 3],
[1, 3],
[2],
[2, 3],
[3]
]
总结
通过这种方法,我们利用循环和递归的结合来生成所有子集,具有以下优点:
- 简洁性:直接在递归开始前将当前子集加入结果集,使得逻辑更为清晰。
- 控制可选范围:通过循环的起始索引控制可选元素,避免了重复选择。
- 自然结束递归:没有显式设置递归出口,而是通过循环的控制自然结束递归。
这种方法在实现上比之前的方法稍微简洁,同时也更容易理解。通过掌握这两种方法,你可以在处理类似问题时灵活选择最合适的解法。
如果你还有其他问题或需要进一步的帮助,请随时告诉我!