以下是力扣子集问题(LeetCode 78. Subsets)的C++解法,结合回溯算法的核心思想实现。问题要求:给定一个整数数组(元素互不相同),返回所有可能的子集(幂集),解集不能包含重复子集。
1. 回溯算法核心思想
回溯算法通过递归探索所有可能的选择路径,每一步决策时记录当前状态,并在回溯时撤销选择。对于子集问题,每个元素有选 与不选两种状态,需遍历所有组合:
-
选择路径:将当前元素加入临时子集,递归处理后续元素。
-
撤销选择:移除当前元素,跳过该元素继续递归。
-
终止条件:处理完所有元素,将当前子集加入结果集。
2. 子集问题实现(元素不重复)
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1: 输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2: 输入:nums = [0] 输出:[[],[0]]
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> ans;
vector<int> path;
backtrack(nums, 0, path, ans);
return ans;
}
private:
void backtrack(vector<int>& nums, int start, vector<int>& path, vector<vector<int>>& ans) {
ans.push_back(path); // 记录当前路径(子集)
for (int i = start; i < nums.size(); i++) {
path.push_back(nums[i]); // 选择当前元素
backtrack(nums, i + 1, path, ans); // 递归处理后续元素
path.pop_back(); // 撤销选择(回溯)
}
}
};
关键点:
-
**
start
参数** :避免重复组合(如[1,2]
和[2,1]
被视为重复),确保每次递归从下一索引开始。 -
空集处理 :首次调用
ans.push_back(path)
会记录空集。 -
时间复杂度:O(n × 2ⁿ),共2ⁿ个子集,每个子集复制需O(n)时间。
-
空间复杂度:O(n),递归栈深度为n。
3. 子集II(元素可能重复)
若数组包含重复元素(LeetCode 90. Subsets II),需去重:
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。 解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1: 输入:nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2: 输入:nums = [0] 输出:[[],[0]]
-
排序数组:使重复元素相邻。
-
跳过重复值:同一层递归中,若当前元素与前一个相同且未被选择,则跳过。
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end()); // 排序
vector<vector<int>> ans;
vector<int> path;
backtrack(nums, 0, path, ans);
return ans;
}private:
void backtrack(vector<int>& nums, int start, vector<int>& path, vector<vector<int>>& ans) {
ans.push_back(path);
for (int i = start; i < nums.size(); i++) {
// 跳过同一层中的重复元素(i > start 确保非首元素)
if (i > start && nums[i] == nums[i - 1]) continue;
path.push_back(nums[i]);
backtrack(nums, i + 1, path, ans);
path.pop_back();
}
}
};
4. 复杂度分析
情况 | 时间复杂度 | 空间复杂度 | 核心操作 |
---|---|---|---|
元素不重复 | O(n × 2ⁿ) | O(n) | 回溯遍历所有子集 |
元素重复 | O(n × 2ⁿ) | O(n) | 排序 + 跳过重复值 |
注:子集问题是回溯算法的基础,其思想可扩展至组合、排列等问题(如全排列、N皇后)。
问题1:
// 在for循环内添加:
if (i > start && nums[i] == nums[i-1])
continue; // 跳过重复元素 没理解,请你解释说明
【解答】你提到的这段代码是回溯算法中处理含重复元素数组时常用的去重技巧。核心目的是避免在同一层级生成重复的子集/组合。下面详细解释其工作原理:
📌 代码片段解析
if (i > start && nums[i] == nums[i-1])
continue; // 跳过重复元素
关键条件分解:
-
**
i > start
**-
表示当前遍历位置 不是本层的第一个元素。
-
start
是当前递归层级的起始索引(即上一层选择的元素位置+1)。 -
当
i > start
时,说明当前元素 不是本层第一个被处理的元素,可能与前一个元素构成重复。
-
-
**
nums[i] == nums[i-1]
**-
检测当前元素是否与前一个元素相同。
-
前提 :数组必须已排序(
sort(nums)
),使相同元素相邻。
-
组合作用:
当这两个条件同时满足时,说明当前元素与前一个元素相同,且前一个相同元素已在本层被处理过。此时跳过当前元素,避免生成重复子集。
🌰 实例演示(以 nums = [1, 2, 2]
为例)
假设数组已排序为 [1, 2, 2]
,回溯树结构如下:
graph TD
A[根节点:start=0] --> B[选1:start=1]
A --> C[不选1:start=1]
B --> D[选第一个2:start=2]
B --> E[选第二个2:触发跳过]
D --> F[选第二个2:生成[1,2,2]]
D --> G[不选2:生成[1]]
C --> H[选第一个2:start=2]
C --> I[选第二个2:触发跳过]
H --> J[选第二个2:生成[2,2]]
H --> K[不选2:生成[2]]
关键步骤:
-
第一层(
start=0
)-
选择
1
→ 进入第二层(start=1
)。 -
不选
1
→ 进入第二层(start=1
)。
-
-
第二层(
start=1
)-
**选择第一个
2
**:- 进入第三层(
start=2
),生成子集[1,2]
→ 继续选第二个2
生成[1,2,2]
。
- 进入第三层(
-
**试图选第二个
2
**:-
此时
i=2
(第二个2
),满足i > start
(2 > 1
)且nums[2] == nums[1]
→ 触发continue
跳过。 -
原因 **:****前一个相同元素(第一个
2
)已在本层被处理过,再选第二个2
会生成重复子集[1,2]
(与选第一个2
的结果重复)**。
-
-
-
不选
1
的分支:-
选择第一个
2
→ 生成[2]
→ 继续选第二个2
生成[2,2]
。 -
试图选第二个
2
时同样触发跳过(逻辑同上)。
-
⚠️ 注意事项
-
必须排序数组
-
未排序时相同元素不相邻,无法通过
nums[i] == nums[i-1]
检测重复。 -
示例:若
nums = [2, 1, 2]
,排序后为[1, 2, 2]
才能正确去重。
-
-
区分「树层去重」与「树枝去重」
-
树层去重:同一层级跳过重复元素(本代码的作用)。
-
树枝去重 :不同树枝允许重复元素(如
[1,2,2]
是允许的,因为两个2
在不同层级)。
-
-
去重的本质
-
通过跳过同一层级中非首个出现的重复元素,确保每个唯一组合仅生成一次。
-
例如
[2,2]
是合法的,因为第一个2
在第一层被选,第二个2
在第二层被选(非同一层)。
-
💡 总结
这段代码通过 **i > start
定位层级位置** + **nums[i] == nums[i-1]
检测重复值** ,高效跳过了同一层级中的冗余分支。其核心思想是:当相同元素在本层已出现过时,后续相同元素直接跳过,避免生成重复解。结合排序预处理,可完美解决子集/组合类问题的去重需求。