【LeetCode热题100道笔记】子集

题目描述

给你一个整数数组 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~n)分层递归,通过“固定当前子集大小+从指定索引开始遍历”避免重复:

  1. 子集需区分“组合”(元素无顺序),因此每次递归从curIndex+1开始遍历,确保元素只按“从左到右”的顺序选择,不重复枚举(如[1,2]不会再出现[2,1]);
  2. size控制当前要生成的子集长度(从1到n),dfs函数仅生成固定长度的子集,最终汇总所有长度的子集(含初始空集)。

算法过程

  1. 初始化:结果列表result先加入空集,创建used数组标记元素是否已选,path存储当前子集;
  2. 分层递归:循环控制子集大小size(从1到n),对每个size调用dfs生成该长度的所有子集;
  3. DFS生成子集
    • 终止条件:若path长度等于size,将当前子集加入结果,返回;
    • 遍历逻辑:从curIndex开始遍历数组,未选中的元素加入path、标记used,递归进入下一层(索引+1),回溯时弹出元素、取消标记;
  4. 返回结果:汇总所有长度的子集,返回result

时空复杂度

  • 时间复杂度:O(n·2ⁿ)
    总子集数为2ⁿ,每个子集的生成需遍历对应长度的元素,总操作次数约为n·2ⁿ(如n=3时,子集总元素数为0+1×3+2×3+3×1=12=3×4=3×2²)。
  • 空间复杂度:O(n)
    递归深度最大为n(生成含n个元素的子集时),pathused数组的长度均为n,额外空间为O(n)(结果存储不计入额外空间)。

代码

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function(nums) {
    const result = [[]];
    const n = nums.length;
    const used = Array(n).fill(false);
    let path = [];

    const dfs = function(size, curIndex) {
        if (path.length === size) {
            result.push([...path]);
            return;
        }

        for (let i = curIndex; i < n; i++) {
            if (used[i]) continue;
            if (path.length < size) {
                path.push(nums[i]);
                used[i] = true;
                dfs(size, i+1);
                path.pop();
                used[i] = false;
            }
        }
    };

    for (let i = 1; i <= n; i++) {
        dfs(i, 0);
    }

    return result;
};

思考二:递归回溯(元素“选/不选”二元决策)

核心思路是将子集生成转化为每个元素的“选或不选”二元决策,无需分层控制子集大小:

  1. 按数组索引顺序遍历(从0到n-1),每个元素仅需做“加入当前子集”或“不加入当前子集”两种选择,天然覆盖所有子集情况(空集、单元素集、多元素集);
  2. 递归到数组末尾(索引i===n)时,当前路径就是一个完整子集,直接加入结果;
  3. 无需used数组标记,因索引严格递增(每次递归i+1),元素不会重复选择,从根源避免重复子集。

算法过程

  1. 初始化:结果列表result存储所有子集,path存储当前正在构建的子集;
  2. 递归入口:从索引0开始调用dfs(0),处理第一个元素;
  3. 递归逻辑(处理索引i的元素):
    • 终止条件:若i===n(遍历完所有元素),将当前path的副本加入result,返回;
    • 选择1(选当前元素):将nums[i]加入path,递归处理下一个元素(i+1),回溯时弹出nums[i](恢复路径);
    • 选择2(不选当前元素):直接递归处理下一个元素(i+1),不修改path
  4. 返回结果:递归结束后,result包含所有子集,返回即可。

复杂度分析

时间复杂度:O(n×2n)O(n \times 2^n)O(n×2n)
  • 算法的本质是对每个元素做“选/不选”的二元决策,因此共产生 2n2^n2n 个不同的子集(包括空集),这是子集总数的数学极限。
  • 每个子集在加入结果集时,需要对当前路径 path 进行复制([...path]),复制操作的时间复杂度为 O(n)O(n)O(n)(路径最长为 nnn)。
  • 因此总时间复杂度为子集数量与单个子集复制时间的乘积,即 O(n×2n)O(n \times 2^n)O(n×2n)
空间复杂度:O(n)O(n)O(n)
  • 递归栈深度:递归的最大深度为 nnn(从索引 000 递归到 nnn),占用 O(n)O(n)O(n) 栈空间。
  • 路径存储path 数组最多存储 nnn 个元素(完整子集),占用 O(n)O(n)O(n) 空间。
  • 结果集 result 存储所有子集,属于输出必要空间,通常不计入算法额外空间复杂度。

代码

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function(nums) {
    const result = [];
    const n = nums.length;
    let path = [];

    const dfs = function(i) {
        if (i === n) {
            result.push([...path]);
            return;
        }

        path.push(nums[i]);
        dfs(i+1);
        path.pop();
        dfs(i+1);
    };

    dfs(0);

    return result;
};

思考三:迭代+位掩码(二进制枚举)

子集问题的本质是“从n个元素中选择任意k个元素(k=0,1,…,n)”,每个元素有“选”或“不选”两种状态。位掩码法的核心是用二进制数的每一位对应元素的选择状态,将“子集枚举”转化为“二进制数枚举”,通过迭代遍历所有可能的二进制状态,直接映射出所有子集。

这种方法的优势在于:

  1. 逻辑直观:二进制的“0”对应“不选”、“1”对应“选”,状态映射无歧义;
  2. 无递归依赖:避免递归深度过大导致的栈溢出,同时代码更简洁;
  3. 时间可控:总状态数固定为2^n(即1 << n),每个状态处理时间为O(n),整体复杂度清晰可预期。

算法过程

  1. 确定状态总数:n个元素的子集总数为2ⁿ(即1 << n),每个状态对应一个二进制掩码;
  2. 枚举所有掩码:遍历mask从0到2ⁿ-1,每个掩码代表一种元素选择组合;
  3. 解析掩码生成子集:对每个mask,通过mask & (1 << i)判断第i个元素是否选中,收集选中元素组成子集;
  4. 收集结果:将所有子集存入结果列表并返回。

时空复杂度

  • 时间复杂度:O(n·2ⁿ)
    共2ⁿ个状态,每个状态需遍历n个元素判断是否选中,总操作次数为n·2ⁿ。

  • 空间复杂度:O(n)
    除结果存储外,仅需O(n)空间存储当前子集(结果本身的空间复杂度为O(n·2ⁿ),通常不计入算法额外空间)。

代码

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function(nums) {
    const result = [];
    const n = nums.length;
    const totalStatus = 1 << n;
    for (let mask = 0; mask < totalStatus; mask++) {
        const t = [];
        for (let i = 0; i < n; i++) {
            if (mask & (1 << i)) {
                t.push(nums[i]);
            }
        }
        result.push(t);
    }

    return result;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值