【每日算法】LeetCode 78. 子集

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode 78. 子集

1. 题目描述

给定一个整数数组 nums,数组中的元素互不相同。返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例

javascript 复制代码
输入: nums = [1,2,3]
输出: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

前端场景联想

  • 权限系统的所有权限组合
  • 商品筛选器的所有筛选条件组合
  • 表单中可选的字段组合
  • 可视化图表中可显示的数据维度组合

2. 问题分析

2.1 问题本质

子集问题本质上是组合问题,需要找出给定集合的所有可能组合。对于长度为 n 的数组,总共有 2^n 个子集(包括空集)。

2.2 关键特性

  1. 元素唯一性:题目明确数组元素互不相同,无需去重
  2. 幂集性质:结果集合大小为 2^n
  3. 顺序无关:子集内元素顺序不重要,但通常按原数组顺序

2.3 前端视角分析

在前端开发中,这种问题常见于:

  • 动态表单字段组合
  • 可配置组件的参数组合
  • 路由权限的组合验证
  • 数据可视化中的维度组合

3. 解题思路

3.1 思路概览

方法 时间复杂度 空间复杂度 最优解
回溯/DFS O(n × 2^n) O(n)
迭代/BFS O(n × 2^n) O(1)(不考虑输出)
位运算 O(n × 2^n) O(1)(不考虑输出)

最优解 :三种方法时间复杂度相同,但回溯法在可读性和灵活性上更优,尤其适合前端开发者理解和实现。

3.2 详细思路分析

3.2.1 回溯法(深度优先搜索)

核心思想:每个元素有"选"或"不选"两种选择,构建决策树。

3.2.2 迭代法(广度优先搜索)

核心思想:从空集开始,每次添加新元素到已有子集中。

3.2.3 位运算法

核心思想:用二进制位表示元素的选择状态(1选,0不选)。

4. 各思路代码实现

4.1 回溯法实现

javascript 复制代码
/**
 * 回溯法 - 最符合前端思维的模式
 * @param {number[]} nums
 * @return {number[][]}
 */
const subsets = function(nums) {
    const result = [];
    
    /**
     * 回溯函数
     * @param {number} index - 当前处理的元素索引
     * @param {number[]} current - 当前子集
     */
    const backtrack = (index, current) => {
        // 所有元素都已处理,保存当前子集
        if (index === nums.length) {
            result.push([...current]); // 深拷贝
            return;
        }
        
        // 选择1:不包含当前元素
        backtrack(index + 1, current);
        
        // 选择2:包含当前元素
        current.push(nums[index]);
        backtrack(index + 1, current);
        current.pop(); // 回溯,撤销选择
    };
    
    backtrack(0, []);
    return result;
};

// 变体:更直观的循环回溯(前端更常用)
const subsetsWithLoop = function(nums) {
    const result = [];
    
    const dfs = (start, path) => {
        // 每次递归都保存当前路径(子集)
        result.push([...path]);
        
        // 从start开始遍历,避免重复
        for (let i = start; i < nums.length; i++) {
            path.push(nums[i]);      // 做出选择
            dfs(i + 1, path);        // 递归
            path.pop();              // 撤销选择
        }
    };
    
    dfs(0, []);
    return result;
};

4.2 迭代法实现

javascript 复制代码
/**
 * 迭代法 - 类似动态扩展
 * @param {number[]} nums
 * @return {number[][]}
 */
const subsetsIterative = function(nums) {
    // 从空集开始
    let result = [[]];
    
    for (let num of nums) {
        // 获取当前所有子集的数量
        const currentLength = result.length;
        
        // 为每个现有子集添加当前元素
        for (let i = 0; i < currentLength; i++) {
            const newSubset = [...result[i], num];
            result.push(newSubset);
        }
    }
    
    return result;
};

// 更简洁的迭代写法(ES6+)
const subsetsIterativeES6 = (nums) => {
    return nums.reduce(
        (subsets, num) => subsets.concat(
            subsets.map(subset => [...subset, num])
        ),
        [[]]
    );
};

4.3 位运算实现

javascript 复制代码
/**
 * 位运算法 - 利用二进制掩码
 * @param {number[]} nums
 * @return {number[][]}
 */
const subsetsBitwise = function(nums) {
    const n = nums.length;
    const total = 1 << n; // 2^n
    const result = [];
    
    // 遍历所有可能的掩码(0 到 2^n-1)
    for (let mask = 0; mask < total; mask++) {
        const subset = [];
        
        // 检查每个位是否被设置
        for (let i = 0; i < n; i++) {
            // 如果第i位是1,则包含nums[i]
            if (mask & (1 << i)) {
                subset.push(nums[i]);
            }
        }
        
        result.push(subset);
    }
    
    return result;
};

5. 各实现思路对比

特性 回溯法 迭代法 位运算法
时间复杂度 O(n × 2^n) O(n × 2^n) O(n × 2^n)
空间复杂度 O(n) 递归栈 O(1)(不考虑输出) O(1)(不考虑输出)
代码可读性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
前端适用性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
内存效率 中等 较高 最高
扩展性 高(易修改) 中等
学习曲线 平缓 平缓 陡峭
实际应用频率

5.1 优缺点详细分析

回溯法

优点

  1. 代码结构清晰,符合递归思维
  2. 容易理解和调试
  3. 易于修改以适应变化需求(如限制子集大小)
  4. 在前端树形结构操作中常见,思维模式可迁移

缺点

  1. 递归可能导致栈溢出(但此题深度n≤10,安全)
  2. 需要理解递归和回溯的概念
迭代法

优点

  1. 无递归栈开销
  2. 代码相对简洁
  3. 适合理解动态构建过程

缺点

  1. 不如回溯法直观
  2. 当需要条件限制时修改较复杂
位运算法

优点

  1. 性能最优
  2. 数学上最优雅
  3. 内存使用最少

缺点

  1. 可读性差,难以维护
  2. 需要理解位运算
  3. 当n较大时(>32)JavaScript有数字精度问题

6. 总结

6.1 核心要点回顾

  1. 子集问题本质是组合问题,结果数量为 2^n
  2. 回溯法是最平衡的解决方案,特别适合前端开发者
  3. 递归思维在前端开发中极其重要(组件树、DOM树、状态树)
  4. 算法思想比具体实现更重要

6.2 实际前端应用场景

场景1:权限系统
javascript 复制代码
// 用户权限子集检查
const userPermissions = ['read', 'write', 'delete', 'admin'];
const requiredPermissions = ['read', 'write'];

// 检查用户权限是否包含所需权限的所有子集组合
function checkPermission(userPerms, requiredPerms) {
    const userPermSet = new Set(userPerms);
    return requiredPerms.every(perm => userPermSet.has(perm));
}
场景2:商品筛选器
javascript 复制代码
// 电商平台的多条件筛选
const filters = ['price', 'category', 'brand', 'rating'];

// 生成所有可能的筛选组合
function generateFilterCombinations(filters) {
    const result = [];
    
    const dfs = (index, current) => {
        result.push([...current]);
        
        for (let i = index; i < filters.length; i++) {
            current.push(filters[i]);
            dfs(i + 1, current);
            current.pop();
        }
    };
    
    dfs(0, []);
    return result;
}
场景3:动态表单配置
javascript 复制代码
// 根据用户角色显示不同的表单字段组合
const allFormFields = ['name', 'email', 'phone', 'address', 'payment'];
const roleFieldSubsets = {
    guest: ['name', 'email'],
    user: ['name', 'email', 'address'],
    admin: ['name', 'email', 'phone', 'address', 'payment']
};

// 生成可配置的表单字段组合
function getAvailableFields(role) {
    return roleFieldSubsets[role] || [];
}
相关推荐
月明长歌2 小时前
【码道初阶】【Leetcode606】二叉树转字符串:前序遍历 + 括号精简规则,一次递归搞定
java·数据结构·算法·leetcode·二叉树
子枫秋月2 小时前
C++字符串操作与迭代器解析
数据结构·算法
鹿角片ljp2 小时前
力扣234.回文链表-反转后半链表
算法·leetcode·链表
(●—●)橘子……2 小时前
记力扣1471.数组中的k个最强值 练习理解
数据结构·python·学习·算法·leetcode
oioihoii2 小时前
C++共享内存小白入门指南
java·c++·算法
Bruce_kaizy2 小时前
c++图论————图的基本与遍历
c++·算法·图论
l1t2 小时前
利用小米mimo为精确覆盖矩形问题C程序添加打乱函数求出更大的解
c语言·开发语言·javascript·人工智能·算法
亭上秋和景清2 小时前
strlen;strcpy ;strcat
算法
_OP_CHEN2 小时前
【算法基础篇】(三十五)图论基础之最小生成树:从原理到实战,彻底吃透 Prim 与 Kruskal 算法
算法·蓝桥杯·图论·最小生成树·kruskal算法·prim算法·acm/icpc