文章目录
-
- 一、题目概述
- 二、思维导图
- 三、解法一:回溯法(Backtracking)
-
- [1. 原理解析](#1. 原理解析)
- [2. 递归流程图](#2. 递归流程图)
- [3. Java 实现代码](#3. Java 实现代码)
- [4. 时间与空间复杂度](#4. 时间与空间复杂度)
- 四、解法二:迭代法(Iterative)
-
- [1. 原理解析](#1. 原理解析)
- [2. 流程图](#2. 流程图)
- [3. Java 实现代码](#3. Java 实现代码)
- [4. 时间与空间复杂度](#4. 时间与空间复杂度)
- [五、解法三:位运算法(Bit Manipulation)](#五、解法三:位运算法(Bit Manipulation))
-
- [1. 原理解析](#1. 原理解析)
- [2. 位运算流程图](#2. 位运算流程图)
- [3. Java 实现代码](#3. Java 实现代码)
- [4. 时间与空间复杂度](#4. 时间与空间复杂度)
- 六、综合对比
- 七、总结
一、题目概述
题目描述:
给定一个不含重复元素的数组 nums,返回该数组所有可能的子集(幂集)。
示例:
输入:nums = [1,2,3]
输出:[[],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]]
说明:
- 子集的数量为
2^n(n 为数组长度)。 - 子集顺序不重要。
二、思维导图
子集问题 Subsets
基本思路
- 枚举所有元素组合 - 每个元素有"选"或"不选"两种状态 解法类别
回溯法
迭代法
位运算枚举法
分析维度
时间复杂度
空间复杂度
可扩展性
三、解法一:回溯法(Backtracking)
1. 原理解析
思想核心:
我们尝试对数组中的每个元素进行「选」或「不选」,通过回溯递归逐层构建所有可能的子集。
每进入一层递归,都表示我们正在考虑数组中的第 i 个元素是否放入当前子集。
2. 递归流程图
是
否
开始
调用 backtrack([], 0)
idx == nums.length?
加入当前子集到结果集
不选择 nums[idx]
backtrack(current, idx+1)
选择 nums[idx]
backtrack(current+[nums[idx]], idx+1)
返回上一层
3. Java 实现代码
java
import java.util.*;
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
backtrack(res, new ArrayList<>(), nums, 0);
return res;
}
private void backtrack(List<List<Integer>> res, List<Integer> temp, int[] nums, int start) {
res.add(new ArrayList<>(temp));
for (int i = start; i < nums.length; i++) {
temp.add(nums[i]);
backtrack(res, temp, nums, i + 1);
temp.remove(temp.size() - 1);
}
}
}
4. 时间与空间复杂度
- 时间复杂度:
O(2^n)
每个元素可选或不选,共2^n种组合。 - 空间复杂度:
O(n)(递归栈深度 + 临时子集存储)
四、解法二:迭代法(Iterative)
1. 原理解析
每加入一个新元素 x,都在当前已有子集中附加一份"包含该元素"的新子集。
例如:
nums = [1,2,3]
初始: [[]]
加入 1: [[],[1]]
加入 2: [[],[1],[2],[1,2]]
加入 3: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
2. 流程图
初始结果 [[]]
遍历 nums
对当前结果集 res 中每个 subset 添加 nums[i]
更新结果集 res
完成所有遍历 → 返回 res
3. Java 实现代码
java
import java.util.*;
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
res.add(new ArrayList<>());
for (int num : nums) {
List<List<Integer>> newSubsets = new ArrayList<>();
for (List<Integer> subset : res) {
List<Integer> temp = new ArrayList<>(subset);
temp.add(num);
newSubsets.add(temp);
}
res.addAll(newSubsets);
}
return res;
}
}
4. 时间与空间复杂度
- 时间复杂度:
O(2^n * n)
每次复制已有子集列表。 - 空间复杂度:
O(2^n * n)(存储全部子集)
五、解法三:位运算法(Bit Manipulation)
1. 原理解析
对每个整数从 0 到 2^n - 1,用其二进制位来表示「选中状态」:
- 二进制第
i位为 1 表示选中nums[i]; - 为 0 表示未选。
例如:
nums = [1, 2, 3]
000 -> []
001 -> [3]
010 -> [2]
011 -> [2,3]
100 -> [1]
101 -> [1,3]
110 -> [1,2]
111 -> [1,2,3]
2. 位运算流程图
for mask in (0 .. 2^n-1)
初始化空子集 subset
检查每一位 bit 是否为 1
若为1则加入对应 nums[i]
将 subset 加入结果集 res
3. Java 实现代码
java
import java.util.*;
public class Solution {
public List<List<Integer>> subsets(int[] nums) {
int n = nums.length;
List<List<Integer>> res = new ArrayList<>();
for (int mask = 0; mask < (1 << n); mask++) {
List<Integer> subset = new ArrayList<>();
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) {
subset.add(nums[i]);
}
}
res.add(subset);
}
return res;
}
}
4. 时间与空间复杂度
- 时间复杂度:
O(2^n * n) - 空间复杂度:
O(2^n * n)
六、综合对比
| 解法 | 思路特点 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|
| 回溯法 | 递归建树结构,自然 | O(2^n) |
O(n) |
理解递归思路练习 |
| 迭代法 | 动态生成新子集 | O(2^n * n) |
O(2^n * n) |
实现简单,非递归 |
| 位运算法 | 利用二进制状态枚举 | O(2^n * n) |
O(2^n * n) |
高效、直观实现 |
七、总结
- 子集问题是典型的 组合枚举问题,关键在于理解「选与不选」的二元决策。
- 回溯法最直观;迭代法实现简洁;位运算法最具数学美感。
- 掌握这三种思路,将有助于解决其他与「组合 / 幂集 / 枚举」相关的问题,如:
- 子集和问题(Subset Sum)
- 子序列生成
- 组合总和(Combination Sum)