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

【LeetCode】90. 子集 II

文章目录

  • 90. 子集 II
    • 题目描述
    • 示例 1:
    • 示例 2:
    • 提示:
    • 解题思路
      • 问题深度分析
        • 问题本质
        • 核心思想
        • 关键难点分析
        • 典型情况分析
        • 算法对比
      • 算法流程图
        • 主算法流程(回溯+去重)
        • 回溯函数流程
        • 去重策略可视化
      • 复杂度分析
        • 时间复杂度详解
        • 空间复杂度详解
      • 关键优化技巧
        • 技巧1:回溯+去重(最优解法)
        • 技巧2:位运算+去重
        • 技巧3:递归+去重
        • 技巧4:迭代+去重
      • 边界情况处理
      • 测试用例设计
        • 基础测试
        • 简单情况
        • 特殊情况
        • 边界情况
      • 常见错误与陷阱
        • 错误1:去重条件错误
        • 错误2:忘记排序
        • 错误3:状态回溯错误
      • 实战技巧总结
      • 进阶扩展
        • 扩展1:返回子集数量
        • 扩展2:限制子集长度
        • 扩展3:子集和等于目标值
      • 应用场景
    • 代码实现
    • 测试结果
    • 核心收获
    • 应用拓展
    • 完整题解代码

90. 子集 II

题目描述

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10

解题思路

问题深度分析

这是经典的回溯算法问题,也是子集生成的复杂应用。核心在于去重处理,在O(2^n)时间内生成所有不重复的子集。

问题本质

给定包含重复元素的整数数组nums,返回所有可能的子集,解集中不能包含重复的子集。

核心思想

回溯 + 去重

  1. 排序预处理:先对数组排序,使相同元素相邻
  2. 回溯生成:使用回溯算法生成所有子集
  3. 去重策略:跳过重复元素,避免生成重复子集
  4. 剪枝优化:在回溯过程中进行剪枝

关键技巧

  • 排序后相同元素相邻,便于去重
  • 使用visited数组或索引控制去重
  • 在回溯过程中跳过重复元素
  • 理解去重的时机和条件
关键难点分析

难点1:去重策略的理解

  • 需要理解何时跳过重复元素
  • 相同元素中,只有第一个可以自由选择
  • 后续相同元素只有在前面元素被选择时才能选择

难点2:回溯状态的维护

  • 需要维护当前路径状态
  • 需要正确更新visited状态
  • 需要正确回溯状态

难点3:剪枝条件的实现

  • 需要正确实现剪枝条件
  • 避免无效的递归调用
  • 提高算法效率
典型情况分析

情况1:一般情况

nums = [1,2,2]
排序后: [1,2,2]回溯过程:
- 选择1: [1]
- 选择1,2: [1,2]
- 选择1,2,2: [1,2,2]
- 选择2: [2]
- 选择2,2: [2,2]
- 不选择任何: []结果: [[],[1],[1,2],[1,2,2],[2],[2,2]]

情况2:单元素

nums = [0]
结果: [[],[0]]

情况3:全部相同

nums = [1,1,1]
结果: [[],[1],[1,1],[1,1,1]]

情况4:无重复

nums = [1,2,3]
结果: [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]
算法对比
算法时间复杂度空间复杂度特点
回溯+去重O(2^n)O(n)最优解法
位运算+去重O(2^n)O(2^n)空间复杂度高
递归+去重O(2^n)O(n)逻辑清晰
迭代+去重O(2^n)O(2^n)避免递归

注:n为数组长度

算法流程图

主算法流程(回溯+去重)
开始: nums
排序数组
初始化结果集和路径
调用回溯函数
返回结果
回溯函数流程
回溯函数: start, path
添加当前路径到结果
start >= len?
返回
遍历剩余元素
检查去重条件
跳过重复?
跳过当前元素
选择当前元素
递归调用
撤销选择
继续下一个元素
去重策略可视化
graph LRsubgraph 排序后数组A1[1,2,2,3]endsubgraph 去重策略B1[选择第一个2]B2[跳过第二个2]B3[只有当前面2被选择时,才能选择后面的2]endsubgraph 回溯树C1[[]]C2[1]C3[1,2]C4[1,2,2]C5[1,2,2,3]C6[1,3]C7[2]C8[2,2]C9[2,2,3]C10[2,3]C11[3]endA1 --> B1B1 --> B2B2 --> B3C1 --> C2C2 --> C3C3 --> C4C4 --> C5C3 --> C6C1 --> C7C7 --> C8C8 --> C9C7 --> C10C1 --> C11

复杂度分析

时间复杂度详解

回溯算法:O(2^n)

  • 需要生成2^n个子集
  • 每个子集的生成时间为O(n)
  • 去重操作的时间复杂度为O(1)
  • 总时间:O(2^n)

位运算算法:O(2^n)

  • 遍历0到2^n-1的每个数
  • 检查每个子集是否重复
  • 总时间:O(2^n)
空间复杂度详解

回溯算法:O(n)

  • 递归栈深度为O(n)
  • 路径数组长度为O(n)
  • 总空间:O(n)

关键优化技巧

技巧1:回溯+去重(最优解法)
func subsetsWithDup(nums []int) [][]int {// 排序,使相同元素相邻sort.Ints(nums)var result [][]intvar path []intvar backtrack func(int)backtrack = func(start int) {// 添加当前路径到结果temp := make([]int, len(path))copy(temp, path)result = append(result, temp)// 遍历剩余元素for i := start; i < len(nums); i++ {// 去重:跳过重复元素if i > start && nums[i] == nums[i-1] {continue}// 选择当前元素path = append(path, nums[i])backtrack(i + 1)path = path[:len(path)-1] // 撤销选择}}backtrack(0)return result
}

优势

  • 时间复杂度:O(2^n)
  • 空间复杂度:O(n)
  • 逻辑清晰,易于理解
技巧2:位运算+去重
func subsetsWithDup(nums []int) [][]int {sort.Ints(nums)var result [][]intseen := make(map[string]bool)n := len(nums)for mask := 0; mask < (1 << n); mask++ {var subset []intfor i := 0; i < n; i++ {if (mask>>i)&1 == 1 {subset = append(subset, nums[i])}}// 生成子集的字符串表示key := fmt.Sprintf("%v", subset)if !seen[key] {seen[key] = trueresult = append(result, subset)}}return result
}

特点:使用位运算生成所有子集,用哈希表去重

技巧3:递归+去重
func subsetsWithDup(nums []int) [][]int {sort.Ints(nums)var result [][]intvar dfs func(int, []int)dfs = func(start int, path []int) {// 添加当前路径temp := make([]int, len(path))copy(temp, path)result = append(result, temp)for i := start; i < len(nums); i++ {if i > start && nums[i] == nums[i-1] {continue}path = append(path, nums[i])dfs(i+1, path)path = path[:len(path)-1]}}dfs(0, []int{})return result
}

特点:使用递归DFS,逻辑清晰

技巧4:迭代+去重
func subsetsWithDup(nums []int) [][]int {sort.Ints(nums)result := [][]int{{}}for i := 0; i < len(nums); i++ {size := len(result)start := 0// 如果当前元素与前一个元素相同,只处理新添加的子集if i > 0 && nums[i] == nums[i-1] {start = size / 2}for j := start; j < size; j++ {newSubset := make([]int, len(result[j]))copy(newSubset, result[j])newSubset = append(newSubset, nums[i])result = append(result, newSubset)}}return result
}

特点:使用迭代方法,避免递归

边界情况处理

  1. 空数组:返回包含空集的数组
  2. 单元素:返回空集和单元素集
  3. 全部相同:返回所有长度的子集
  4. 无重复:返回所有可能的子集

测试用例设计

基础测试
输入: nums = [1,2,2]
输出: [[],[1],[1,2],[1,2,2],[2],[2,2]]
说明: 一般情况
简单情况
输入: nums = [0]
输出: [[],[0]]
说明: 单元素
特殊情况
输入: nums = [1,1,1]
输出: [[],[1],[1,1],[1,1,1]]
说明: 全部相同
边界情况
输入: nums = []
输出: [[]]
说明: 空数组

常见错误与陷阱

错误1:去重条件错误
// ❌ 错误:去重条件不正确
if i > 0 && nums[i] == nums[i-1] {continue
}// ✅ 正确:去重条件
if i > start && nums[i] == nums[i-1] {continue
}
错误2:忘记排序
// ❌ 错误:没有排序
func subsetsWithDup(nums []int) [][]int {// 直接开始回溯,无法正确去重
}// ✅ 正确:先排序
func subsetsWithDup(nums []int) [][]int {sort.Ints(nums)// 然后开始回溯
}
错误3:状态回溯错误
// ❌ 错误:没有正确回溯状态
path = append(path, nums[i])
backtrack(i + 1)
// 忘记撤销选择// ✅ 正确:正确回溯状态
path = append(path, nums[i])
backtrack(i + 1)
path = path[:len(path)-1] // 撤销选择

实战技巧总结

  1. 排序预处理:先排序使相同元素相邻
  2. 去重策略:跳过重复元素
  3. 回溯模板:选择-递归-撤销
  4. 剪枝优化:避免无效递归
  5. 状态管理:正确维护回溯状态

进阶扩展

扩展1:返回子集数量
func countSubsetsWithDup(nums []int) int {// 返回不重复子集的数量// ...
}
扩展2:限制子集长度
func subsetsWithDupFixedLength(nums []int, k int) [][]int {// 返回长度为k的不重复子集// ...
}
扩展3:子集和等于目标值
func subsetsWithDupSum(nums []int, target int) [][]int {// 返回和等于target的不重复子集// ...
}

应用场景

  1. 组合数学:子集生成和计数
  2. 算法竞赛:回溯算法经典应用
  3. 数据处理:数据子集分析
  4. 系统设计:权限组合管理
  5. 机器学习:特征子集选择

代码实现

本题提供了四种不同的解法,重点掌握回溯+去重算法。

测试结果

测试用例回溯+去重位运算+去重递归+去重迭代+去重
基础测试
简单情况
特殊情况
边界情况

核心收获

  1. 回溯算法:子集生成的经典应用
  2. 去重策略:跳过重复元素的技巧
  3. 排序预处理:使相同元素相邻
  4. 状态管理:正确维护回溯状态
  5. 剪枝优化:提高算法效率

应用拓展

  • 组合数学基础
  • 回溯算法应用
  • 数据处理技术
  • 系统设计应用
  • 算法竞赛技巧

完整题解代码

package mainimport ("fmt""sort"
)// =========================== 方法一:回溯+去重(最优解法) ===========================func subsetsWithDup(nums []int) [][]int {// 排序,使相同元素相邻sort.Ints(nums)var result [][]intvar path []intvar backtrack func(int)backtrack = func(start int) {// 添加当前路径到结果temp := make([]int, len(path))copy(temp, path)result = append(result, temp)// 遍历剩余元素for i := start; i < len(nums); i++ {// 去重:跳过重复元素if i > start && nums[i] == nums[i-1] {continue}// 选择当前元素path = append(path, nums[i])backtrack(i + 1)path = path[:len(path)-1] // 撤销选择}}backtrack(0)return result
}// =========================== 方法二:位运算+去重 ===========================func subsetsWithDup2(nums []int) [][]int {sort.Ints(nums)var result [][]intseen := make(map[string]bool)n := len(nums)for mask := 0; mask < (1 << n); mask++ {var subset []intfor i := 0; i < n; i++ {if (mask>>i)&1 == 1 {subset = append(subset, nums[i])}}// 生成子集的字符串表示key := fmt.Sprintf("%v", subset)if !seen[key] {seen[key] = trueresult = append(result, subset)}}return result
}// =========================== 方法三:递归+去重 ===========================func subsetsWithDup3(nums []int) [][]int {sort.Ints(nums)var result [][]intvar dfs func(int, []int)dfs = func(start int, path []int) {// 添加当前路径temp := make([]int, len(path))copy(temp, path)result = append(result, temp)for i := start; i < len(nums); i++ {if i > start && nums[i] == nums[i-1] {continue}path = append(path, nums[i])dfs(i+1, path)path = path[:len(path)-1]}}dfs(0, []int{})return result
}// =========================== 方法四:迭代+去重(优化版) ===========================func subsetsWithDup4(nums []int) [][]int {sort.Ints(nums)result := [][]int{{}}prevSize := 0for i := 0; i < len(nums); i++ {size := len(result)start := 0// 如果当前元素与前一个元素相同,只从上一轮新增的子集开始if i > 0 && nums[i] == nums[i-1] {start = prevSize}prevSize = sizefor j := start; j < size; j++ {newSubset := make([]int, len(result[j]))copy(newSubset, result[j])newSubset = append(newSubset, nums[i])result = append(result, newSubset)}}return result
}// =========================== 测试代码 ===========================func main() {fmt.Println("=== LeetCode 90: 子集 II ===\n")testCases := []struct {name     stringnums     []intexpected [][]int}{{name:     "Test1: Basic case",nums:     []int{1, 2, 2},expected: [][]int{{}, {1}, {1, 2}, {1, 2, 2}, {2}, {2, 2}},},{name:     "Test2: Single element",nums:     []int{0},expected: [][]int{{}, {0}},},{name:     "Test3: All same elements",nums:     []int{1, 1, 1},expected: [][]int{{}, {1}, {1, 1}, {1, 1, 1}},},{name:     "Test4: No duplicates",nums:     []int{1, 2, 3},expected: [][]int{{}, {1}, {1, 2}, {1, 2, 3}, {1, 3}, {2}, {2, 3}, {3}},},{name:     "Test5: Empty array",nums:     []int{},expected: [][]int{{}},},{name:     "Test6: Two duplicates",nums:     []int{4, 4, 4, 1, 4},expected: [][]int{{}, {1}, {1, 4}, {1, 4, 4}, {1, 4, 4, 4}, {1, 4, 4, 4, 4}, {4}, {4, 4}, {4, 4, 4}, {4, 4, 4, 4}},},}methods := map[string]func([]int) [][]int{"回溯+去重(最优解法)": subsetsWithDup,"位运算+去重":      subsetsWithDup2,"递归+去重":       subsetsWithDup3,"迭代+去重":       subsetsWithDup4,}for name, method := range methods {fmt.Printf("方法%s:%s\n", name, name)passCount := 0for i, tt := range testCases {got := method(tt.nums)// 验证结果是否正确valid := isValidSubsets(got, tt.expected)status := "✅"if !valid {status = "❌"} else {passCount++}fmt.Printf("  测试%d: %s\n", i+1, status)if status == "❌" {fmt.Printf("    输入: %v\n", tt.nums)fmt.Printf("    输出: %v\n", got)fmt.Printf("    期望: %v\n", tt.expected)}}fmt.Printf("  通过: %d/%d\n\n", passCount, len(testCases))}
}// 验证子集是否正确
func isValidSubsets(got, expected [][]int) bool {if len(got) != len(expected) {return false}// 将结果转换为集合进行比较gotSet := make(map[string]bool)for _, subset := range got {key := fmt.Sprintf("%v", subset)gotSet[key] = true}expectedSet := make(map[string]bool)for _, subset := range expected {key := fmt.Sprintf("%v", subset)expectedSet[key] = true}// 比较两个集合for key := range gotSet {if !expectedSet[key] {return false}}for key := range expectedSet {if !gotSet[key] {return false}}return true
}
http://www.dtcms.com/a/542189.html

相关文章:

  • 做公司网站阿里网站建设及维护服务
  • 用html做班级网站网站推广策划方案的主要内容?
  • MySQL忘记root密码或者root密码错误
  • “三断”极端环境下,自组网通信如何赋能关键领域救援?
  • 模板网站最大缺点网站建设公司咨
  • AI大模型-深度学习相关概念
  • 网站建设有前途wordpress汉化自适应主题
  • 统计 vs 机器学习:从参数到模式识别的转变
  • 做网站时版权怎么写青岛最新发生的新闻
  • 做自媒体的网站名字中英文网站如何做思路
  • 什么网站可以做单词书汽车之家汽车报价大全网页版
  • 廊坊永清网站建设什么网站可以做外贸
  • 贵州城乡建设厅城乡建设网站手机网站建设用乐云seo
  • 太原网站科技公司大专千万不要报软件技术
  • MCP | LLM | Python类型的Stdio/SSE/HTTP模式的简单示例
  • 网站备案账号密码哪儿有那种网站
  • 育贤网站建设wordpress默认小工具
  • 代做淘宝联盟网站网站通知发送邮件
  • 增强版计算机管理脚本
  • 动易网站后台管理系统wordpress homepage plugin
  • 招聘网站对比这么做网站开发实训的心得
  • 河南平台网站建设制作云南网络营销公司
  • 三维重建【0-C】3D Gaussian Splatting:相机标定原理与步骤
  • 好的建站平台简述网站建设的主要内容
  • 公司网站无法收录百度推广是干什么的
  • 可上传多个视频的网站建设沈阳制作网站建站
  • 百度提交网站多久收录怎么搭建个人博客
  • PE之文件结构
  • wordpress创建多站点小型教育网站的开发与建设
  • 如何构建构高性能、高可用、可扩展的集群?