# 子集 (Subsets) ## 题目描述 给你一个整数数组 `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` 中的所有元素 **互不相同** ## 解题思路 ### 方法一:回溯法(推荐) **核心思想:**对于每个元素,可以选择包含或不包含。使用回溯法生成所有可能的组合。 **算法步骤:** 1. 初始化结果数组和当前子集 2. 定义回溯函数 `backtrack(start)`: - 将当前子集加入结果 - 从 `start` 开始遍历,依次尝试包含每个元素 - 递归调用后撤销选择(回溯) ### 方法二:迭代法(位掩码) **核心思想:**子集可以用二进制表示。对于 n 个元素,共有 2^n 个子集。 **算法步骤:** 1. 计算子集总数 `total = 1 << n` 2. 对于每个数字 `i` 从 0 到 `total-1`: - 将 `i` 的二进制表示转换为子集 - 第 `j` 位为 1 表示包含 `nums[j]` ### 方法三:级联法 **核心思想:**对于已有的每个子集,通过添加当前元素生成新的子集。 **算法步骤:** 1. 初始化结果为 `[[]]` 2. 对于每个元素: - 取出所有已有子集 - 将当前元素添加到每个子集 - 将新子集加入结果 ## 代码实现 ### Go 实现(回溯法) ```go package main import "fmt" func subsets(nums []int) [][]int { result := [][]int{} current := []int{} var backtrack func(start int) backtrack = func(start int) { // 将当前子集加入结果(需要复制) temp := make([]int, len(current)) copy(temp, current) result = append(result, temp) // 从 start 开始尝试包含每个元素 for i := start; i < len(nums); i++ { // 选择当前元素 current = append(current, nums[i]) // 递归处理下一个元素 backtrack(i + 1) // 撤销选择(回溯) current = current[:len(current)-1] } } backtrack(0) return result } // 测试用例 func main() { // 测试用例1 nums1 := []int{1, 2, 3} fmt.Printf("输入: %v\n", nums1) fmt.Printf("输出: %v\n", subsets(nums1)) // 测试用例2 nums2 := []int{0} fmt.Printf("\n输入: %v\n", nums2) fmt.Printf("输出: %v\n", subsets(nums2)) // 测试用例3 nums3 := []int{1, 2} fmt.Printf("\n输入: %v\n", nums3) fmt.Printf("输出: %v\n", subsets(nums3)) } ``` ### Java 实现(回溯法) ```java import java.util.ArrayList; import java.util.List; public class Subsets { public List> subsets(int[] nums) { List> result = new ArrayList<>(); List current = new ArrayList<>(); backtrack(result, current, nums, 0); return result; } private void backtrack(List> result, List current, int[] nums, int start) { // 将当前子集加入结果 result.add(new ArrayList<>(current)); // 从 start 开始尝试包含每个元素 for (int i = start; i < nums.length; i++) { // 选择当前元素 current.add(nums[i]); // 递归处理下一个元素 backtrack(result, current, nums, i + 1); // 撤销选择(回溯) current.remove(current.size() - 1); } } // 测试用例 public static void main(String[] args) { Subsets solution = new Subsets(); // 测试用例1 int[] nums1 = {1, 2, 3}; System.out.println("输入: [1, 2, 3]"); System.out.println("输出: " + solution.subsets(nums1)); // 测试用例2 int[] nums2 = {0}; System.out.println("\n输入: [0]"); System.out.println("输出: " + solution.subsets(nums2)); // 测试用例3 int[] nums3 = {1, 2}; System.out.println("\n输入: [1, 2]"); System.out.println("输出: " + solution.subsets(nums3)); } } ``` ### Go 实现(迭代法-位掩码) ```go func subsetsBitMask(nums []int) [][]int { n := len(nums) total := 1 << n // 2^n 个子集 result := make([][]int, 0, total) for mask := 0; mask < total; mask++ { subset := []int{} for i := 0; i < n; i++ { // 检查第 i 位是否为 1 if mask&(1<> subsetsBitMask(int[] nums) { int n = nums.length; int total = 1 << n; // 2^n 个子集 List> result = new ArrayList<>(); for (int mask = 0; mask < total; mask++) { List subset = new ArrayList<>(); for (int i = 0; i < n; i++) { // 检查第 i 位是否为 1 if ((mask & (1 << i)) != 0) { subset.add(nums[i]); } } result.add(subset); } return result; } ``` ### Go 实现(级联法) ```go func subsetsCascade(nums []int) [][]int { result := [][]int{{}} // 初始化为空集 for _, num := range nums { // 对于每个已有子集,添加当前元素生成新子集 newSubsets := make([][]int, 0, len(result)) for _, subset := range result { newSubset := make([]int, len(subset)+1) copy(newSubset, subset) newSubset[len(subset)] = num newSubsets = append(newSubsets, newSubset) } result = append(result, newSubsets...) } return result } ``` ## 复杂度分析 ### 回溯法 - **时间复杂度:** O(n × 2^n) - 共有 2^n 个子集 - 每个子集的复制需要 O(n) 时间 - **空间复杂度:** O(n) - 递归栈深度最大为 n - 不包括存储结果的空间 ### 迭代法(位掩码) - **时间复杂度:** O(n × 2^n) - 需要生成 2^n 个子集 - 每个子集需要 O(n) 时间构建 - **空间复杂度:** O(1) - 只使用了常数级别的额外空间(不包括结果) ### 级联法 - **时间复杂度:** O(n × 2^n) - 每次迭代都会将子集数量翻倍 - 总共需要处理 n 次 - **空间复杂度:** O(n × 2^n) - 需要存储所有子集 ## 进阶问题 ### Q1: 如果数组中有重复元素,应该如何处理? **A:** 需要先排序,然后在回溯时跳过重复元素。 ```go func subsetsWithDup(nums []int) [][]int { sort.Ints(nums) result := [][]int{} current := []int{} var backtrack func(start int) backtrack = func(start int) { temp := make([]int, len(current)) copy(temp, current) result = append(result, temp) for i := start; i < len(nums); i++ { // 跳过重复元素 if i > start && nums[i] == nums[i-1] { continue } current = append(current, nums[i]) backtrack(i + 1) current = current[:len(current)-1] } } backtrack(0) return result } ``` ### Q2: 如果要求子集的大小恰好为 k,应该如何修改? **A:** 在回溯时添加终止条件。 ```go func subsetsK(nums []int, k int) [][]int { result := [][]int{} current := []int{} var backtrack func(start int) backtrack = func(start int) { if len(current) == k { temp := make([]int, len(current)) copy(temp, current) result = append(result, temp) return } for i := start; i < len(nums); i++ { current = append(current, nums[i]) backtrack(i + 1) current = current[:len(current)-1] } } backtrack(0) return result } ``` ## P7 加分项 ### 1. 深度理解:为什么子集问题适合用回溯法? **回溯法的本质:** - 在解空间树中进行深度优先搜索 - 每个节点代表一个决策(包含或不包含当前元素) - 通过撤销选择(回溯)来探索所有可能 **为什么适合子集问题:** 1. **决策清晰:**每个元素只有两种选择(包含或不包含) 2. **无后效性:**当前选择不影响之前的选择 3. **边界明确:**子集大小从 0 到 n ### 2. 实战扩展:组合与排列 **组合问题:**从 n 个元素中选 k 个,不考虑顺序 **排列问题:**从 n 个元素中选 k 个,考虑顺序 ```go // 组合 func combine(n int, k int) [][]int { result := [][]int{} current := []int{} var backtrack func(start int) backtrack = func(start int) { if len(current) == k { temp := make([]int, len(current)) copy(temp, current) result = append(result, temp) return } for i := start; i <= n; i++ { current = append(current, i) backtrack(i + 1) current = current[:len(current)-1] } } backtrack(1) return result } // 排列 func permute(nums []int) [][]int { result := [][]int{} current := []int{} used := make([]bool, len(nums)) var backtrack func() backtrack = func() { if len(current) == len(nums) { temp := make([]int, len(current)) copy(temp, current) result = append(result, temp) return } for i := 0; i < len(nums); i++ { if used[i] { continue } current = append(current, nums[i]) used[i] = true backtrack() current = current[:len(current)-1] used[i] = false } } backtrack() return result } ``` ### 3. 变形题目 #### 变形1:子集 II(有重复元素) **LeetCode 90:** 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 ```go func subsetsWithDup(nums []int) [][]int { sort.Ints(nums) result := [][]int{} current := []int{} var backtrack func(start int) backtrack = func(start int) { temp := make([]int, len(current)) copy(temp, current) result = append(result, temp) for i := start; i < len(nums); i++ { if i > start && nums[i] == nums[i-1] { continue } current = append(current, nums[i]) backtrack(i + 1) current = current[:len(current)-1] } } backtrack(0) return result } ``` ### 4. 相关题目推荐 - LeetCode 78: 子集(本题) - LeetCode 90: 子集 II - LeetCode 77: 组合 - LeetCode 46: 全排列 - LeetCode 47: 全排列 II