当前位置: 首页 > news >正文

LeetCode第78题_子集

LeetCode第78题:子集

题目描述

给你一个整数数组 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. 定义一个递归函数 backtrack(start, path),其中 start 表示当前考虑的元素位置,path 表示当前已选择的元素集合
  2. 在每一步中,我们都有两个选择:选择当前元素或不选择当前元素
  3. 对于每个元素,我们都将当前路径添加到结果集中(因为每个路径都是一个有效的子集)
  4. 然后继续递归处理下一个元素

方法二:迭代法

迭代法的思路是,从空集开始,每次将当前所有子集与新元素组合,生成新的子集。

  1. 初始化结果集为空集 [[]]
  2. 遍历数组中的每个元素 num
  3. 对于每个元素,将当前结果集中的每个子集都复制一份,并在复制的子集中添加当前元素 num
  4. 将这些新生成的子集添加到结果集中

方法三:位运算法

位运算法利用了子集的一个重要特性:对于长度为 n 的数组,共有 2^n 个子集,每个子集可以用一个 n 位的二进制数表示,其中第 i 位为 1 表示选择第 i 个元素,为 0 表示不选择。

  1. 对于长度为 n 的数组,共有 2^n 个子集
  2. 我们可以用 0 到 2^n - 1 的二进制表示来表示所有可能的子集
  3. 对于每个二进制表示,我们检查每一位是否为 1,如果是,则将对应位置的元素添加到当前子集中

关键点

  • 理解子集的性质:长度为 n 的数组共有 2^n 个子集
  • 正确处理递归的终止条件和状态恢复(对于回溯法)
  • 理解位运算在子集生成中的应用(对于位运算法)

算法步骤分析

以回溯法为例:

步骤操作说明
1初始化创建结果集 result 和当前路径 path
2定义回溯函数backtrack(start, path) 用于生成所有可能的子集
3添加当前路径将当前路径添加到结果集中
4选择元素start 开始,考虑每个元素是否选择
5递归调用选择当前元素后,递归处理下一个元素
6撤销选择将最后添加的元素从路径中移除,尝试其他可能的组合
7返回结果返回所有可能的子集

算法可视化

以示例 1 为例,nums = [1,2,3],回溯过程如下:

  1. 初始状态:path = [],将空集添加到结果集,结果集变为 [[]]
  2. 考虑元素 1:
    • 选择元素 1:path = [1],将 [1] 添加到结果集,结果集变为 [[], [1]]
    • 考虑元素 2:
      • 选择元素 2:path = [1, 2],将 [1, 2] 添加到结果集,结果集变为 [[], [1], [1, 2]]
      • 考虑元素 3:
        • 选择元素 3:path = [1, 2, 3],将 [1, 2, 3] 添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3]]
        • 撤销选择 3:path = [1, 2]
      • 撤销选择 2:path = [1]
    • 考虑元素 3:
      • 选择元素 3:path = [1, 3],将 [1, 3] 添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3], [1, 3]]
      • 撤销选择 3:path = [1]
    • 撤销选择 1:path = []
  3. 考虑元素 2:
    • 选择元素 2:path = [2],将 [2] 添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3], [1, 3], [2]]
    • 考虑元素 3:
      • 选择元素 3:path = [2, 3],将 [2, 3] 添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3]]
      • 撤销选择 3:path = [2]
    • 撤销选择 2:path = []
  4. 考虑元素 3:
    • 选择元素 3:path = [3],将 [3] 添加到结果集,结果集变为 [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
    • 撤销选择 3:path = []
  5. 最终结果集为 [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

代码实现

C# 实现

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);
        }
    }
}

Python 实现

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

C++ 实现

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();
        }
    }
};

执行结果

C# 执行结果

  • 执行用时:108 ms,击败了 95.24% 的 C# 提交
  • 内存消耗:41.2 MB,击败了 88.10% 的 C# 提交

Python 执行结果

  • 执行用时:32 ms,击败了 94.56% 的 Python3 提交
  • 内存消耗:16.2 MB,击败了 87.89% 的 Python3 提交

C++ 执行结果

  • 执行用时:0 ms,击败了 100.00% 的 C++ 提交
  • 内存消耗:7.1 MB,击败了 91.23% 的 C++ 提交

代码亮点

  1. 回溯算法的应用:使用回溯算法解决子集问题,代码结构清晰。
  2. 状态恢复:在递归返回后,正确地恢复状态,确保不影响其他分支的探索。
  3. 深拷贝处理:在添加结果时,创建当前路径的副本,避免后续修改影响已添加的结果。
  4. 递归终止条件:不需要显式的终止条件,因为循环会自然终止。
  5. 空间优化:只需要一个路径变量和结果集,空间复杂度为 O(n)。

常见错误分析

  1. 忘记创建路径副本:在将当前路径添加到结果集时,如果不创建副本,后续的修改会影响已添加的结果。
  2. 递归终止条件设置不当:对于子集问题,每个路径都是一个有效的子集,不需要等到路径长度达到某个值才添加到结果集。
  3. 状态恢复不完全:在递归返回后,需要完全恢复状态,否则会影响其他分支的探索。
  4. 循环范围设置错误:循环的起始和结束位置需要正确设置,否则可能漏掉某些子集或产生重复子集。
  5. 没有考虑空集:空集也是一个有效的子集,需要确保结果集中包含空集。

解法比较

解法时间复杂度空间复杂度优点缺点
回溯法O(n * 2^n)O(n)实现简单,适用于所有子集问题递归调用可能导致栈溢出(对于非常大的输入)
迭代法O(n * 2^n)O(n * 2^n)避免递归调用的开销需要额外的空间来存储中间结果
位运算法O(n * 2^n)O(n)实现简洁,直观表示子集对于不熟悉位运算的人可能难以理解

相关题目

  • LeetCode 77. 组合
  • LeetCode 90. 子集 II
  • LeetCode 46. 全排列
  • LeetCode 39. 组合总和

相关文章:

  • ubuntu打包 qt 程序,不用每次都用linuxdeployqt打包
  • mybatisplus 开发流程
  • 1236 - 二分查找
  • jenkins配置连接k8s集群
  • LeetCode和为k的字数组(560题)
  • 【hello git】git 扫盲(add、commit、push、reset、status、log、checkout)
  • C语言学习笔记:初阶指针
  • 在 Maven 中使用 <scope> 元素:全面指南
  • “深入浅出”系列之Linux篇:(10)基于C++实现分布式网络通信RPC框架
  • 软件开发工程师与AI工具
  • MySQL字段内容加解密使用性能验证
  • Linux学习记录1
  • Manus AI Agent介绍总结
  • js正则表达式--标识符(6)
  • 浏览器WEB播放RTSP
  • 键值对(C++实现)
  • 鸿蒙应用开发深度解析:API 14核心特性与实战指南
  • C++ Primer Plus 编程练习题 第三章 处理数据
  • 某得物 - WebView App H5调试
  • dataframe能否把列表里所有的NaN换成列表上一位的数字?
  • 网站建设会面临些什么问题/南京百度seo公司
  • 松江手机网站建设/河北seo
  • 免费发布项目的网站/郑州网站推广哪家专业
  • 平面设计素材网站排名/深圳营销推广引流公司
  • 局域网内建设网站/百度推广后台登录首页
  • 移动手机网站开发/佛山全市核酸检测