给你一个整数数组 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
中的所有元素 互不相同这道题目是经典的子集问题,可以使用多种方法来解决,包括回溯法、迭代法和位运算法。
回溯法是一种通过探索所有可能的候选解来找出所有解的算法。对于子集问题,我们可以考虑每个元素是否被选择,从而生成所有可能的子集。
backtrack(start, path)
,其中 start
表示当前考虑的元素位置,path
表示当前已选择的元素集合迭代法的思路是,从空集开始,每次将当前所有子集与新元素组合,生成新的子集。
[[]]
num
num
位运算法利用了子集的一个重要特性:对于长度为 n 的数组,共有 2^n 个子集,每个子集可以用一个 n 位的二进制数表示,其中第 i 位为 1 表示选择第 i 个元素,为 0 表示不选择。
以回溯法为例:
步骤 | 操作 | 说明 |
---|---|---|
1 | 初始化 | 创建结果集 result 和当前路径 path |
2 | 定义回溯函数 | backtrack(start, path) 用于生成所有可能的子集 |
3 | 添加当前路径 | 将当前路径添加到结果集中 |
4 | 选择元素 | 从 start 开始,考虑每个元素是否选择 |
5 | 递归调用 | 选择当前元素后,递归处理下一个元素 |
6 | 撤销选择 | 将最后添加的元素从路径中移除,尝试其他可能的组合 |
7 | 返回结果 | 返回所有可能的子集 |
以示例 1 为例,nums = [1,2,3]
,回溯过程如下:
path = []
,将空集添加到结果集,结果集变为 [[]]
path = [1]
,将 [1]
添加到结果集,结果集变为 [[], [1]]
path = [1, 2]
,将 [1, 2]
添加到结果集,结果集变为 [[], [1], [1, 2]]
path = [1, 2, 3]
,将 [1, 2, 3]
添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3]]
path = [1, 2]
path = [1]
path = [1, 3]
,将 [1, 3]
添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3], [1, 3]]
path = [1]
path = []
path = [2]
,将 [2]
添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3], [1, 3], [2]]
path = [2, 3]
,将 [2, 3]
添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3]]
path = [2]
path = []
path = [3]
,将 [3]
添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
path = []
[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
public class Solution {
private IList<IList<int>> result = new List<IList<int>>();
public IList<IList<int>> Subsets(int[] nums) {
Backtrack(nums, 0, new List<int>());
return result;
}
private void Backtrack(int[] nums, int start, IList<int> path) {
// 将当前路径添加到结果集中
result.Add(new List<int>(path));
// 从start开始,考虑每个元素是否选择
for (int i = start; i < nums.Length; i++) {
// 选择当前元素
path.Add(nums[i]);
// 递归处理下一个元素
Backtrack(nums, i + 1, path);
// 撤销选择,回溯
path.RemoveAt(path.Count - 1);
}
}
}
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
result = []
def backtrack(start, path):
# 将当前路径添加到结果集中
result.append(path[:])
# 从start开始,考虑每个元素是否选择
for i in range(start, len(nums)):
# 选择当前元素
path.append(nums[i])
# 递归处理下一个元素
backtrack(i + 1, path)
# 撤销选择,回溯
path.pop()
backtrack(0, [])
return result
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> result;
vector<int> path;
backtrack(nums, 0, path, result);
return result;
}
private:
void backtrack(vector<int>& nums, int start, vector<int>& path, vector<vector<int>>& result) {
// 将当前路径添加到结果集中
result.push_back(path);
// 从start开始,考虑每个元素是否选择
for (int i = start; i < nums.size(); i++) {
// 选择当前元素
path.push_back(nums[i]);
// 递归处理下一个元素
backtrack(nums, i + 1, path, result);
// 撤销选择,回溯
path.pop_back();
}
}
};
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
回溯法 | O(n * 2^n) | O(n) | 实现简单,适用于所有子集问题 | 递归调用可能导致栈溢出(对于非常大的输入) |
迭代法 | O(n * 2^n) | O(n * 2^n) | 避免递归调用的开销 | 需要额外的空间来存储中间结果 |
位运算法 | O(n * 2^n) | O(n) | 实现简洁,直观表示子集 | 对于不熟悉位运算的人可能难以理解 |