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

leetcode90.子集II:排序与同层去重的回溯优化策略

一、题目深度解析与重复子集问题

题目描述

给定一个可能包含重复元素的整数数组 nums,返回所有不重复的子集(幂集)。解集不能包含重复的子集,且子集可以按任意顺序排列。例如:

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

核心挑战:重复子集的产生与消除

  1. 重复原因:数组中存在重复元素时,不同的选择路径可能生成相同子集(如选择第一个2和第二个2产生相同子集)
  2. 解决关键:通过排序和回溯时的条件判断,确保相同元素在同一路径中仅被处理一次

二、回溯解法的核心实现与去重逻辑

完整回溯代码实现

class Solution {List<Integer> temp = new LinkedList<>();  // 存储当前子集List<List<Integer>> res = new ArrayList<>();  // 存储所有子集public List<List<Integer>> subsetsWithDup(int[] nums) {Arrays.sort(nums);  // 排序是去重的前提backtracking(nums, 0, temp);return res;}public void backtracking(int[] nums, int start, List<Integer> temp) {res.add(new ArrayList<>(temp));  // 收集当前子集(包括空集)if (start >= nums.length) {  // 终止条件:处理完所有元素return;}for (int i = start; i < nums.length; i++) {// 同层去重:当前元素与前一个相同,且前一个未被处理(i > start)if (i > start && nums[i] == nums[i - 1]) {continue;}temp.add(nums[i]);  // 选择当前元素backtracking(nums, i + 1, temp);  // 递归处理后续元素temp.removeLast();  // 回溯:撤销选择}}
}

核心去重代码解析:

if (i > start && nums[i] == nums[i - 1]) {continue;
}
  • 排序前提Arrays.sort(nums)确保重复元素相邻,为去重提供条件
  • 条件拆解
    1. i > start:当前元素不是当前层的第一个元素(即前一个元素在同一路径中已被处理)
    2. nums[i] == nums[i - 1]:当前元素与前一个元素值相同

三、去重逻辑的数学原理与代码实现

1. 同层去重的核心思想

子集生成的两种重复场景:
  • 不同层重复:路径1→21→2(同一元素的不同副本,合法,如不同层的2)
  • 同层重复:路径2(第一个)→2(第二个)2(第二个)→2(第一个)(非法,生成相同子集)
代码如何避免同层重复:
  • 排序后:重复元素相邻,如[1,2,2]排序后为[1,2,2]
  • 同层判断:在同一层循环中(即同一个父节点的子节点),如果当前元素与前一个元素相同,且前一个元素未被处理(i > start),则跳过当前元素
  • 不同层允许:在不同层(即不同父节点的子节点),允许处理重复元素(如第一个2的子节点和第二个2的子节点独立处理)

2. 条件i > start的关键作用

示例说明:处理nums = [1,2,2]
  • 第一层循环(start=0,处理元素1)

    • i=0:处理1,无重复,正常选择
    • i=1:处理第一个2,i > start(0)true,但nums[1] != nums[0],不跳过
    • i=2:处理第二个2,nums[2] == nums[1]i > start(0),跳过(避免同层重复)
  • 第二层循环(start=1,处理第一个2的子节点)

    • i=1:处理第一个2,start=1i > startfalse,允许处理
    • i=2:处理第二个2,nums[2] == nums[1]i == start(1),不跳过(不同层允许)

四、去重流程深度模拟:以输入[1,2,2]为例

递归调用树与去重节点:

backtracking([1,2,2], 0)
├─ temp=[] → 收集[]
│  ├─ i=0(元素1):temp=[1] → 收集[1]
│  │  ├─ start=1,处理第二个2(i=1):temp=[1,2] → 收集[1,2]
│  │  │  ├─ start=2,处理第三个2(i=2):temp=[1,2,2] → 收集[1,2,2]
│  │  └─ i=2(元素2):nums[2]==nums[1]且i>start(1)? 否(i=2, start=1,i>start为true,但此时是不同层?不,start=1,i=2>start=1,且nums[2]==nums[1],但在第二层循环中,start=1,i=2属于同层吗?
│  ├─ i=1(第一个2):nums[1]==nums[0]? 否,正常处理,temp=[2] → 收集[2]
│  │  └─ start=2,处理第二个2(i=2):temp=[2,2] → 收集[2,2]
│  └─ i=2(第二个2):nums[2]==nums[1]且i>start(0) → 是,跳过(同层重复)
└─ i=1(第一个2,start=0):被i=0处理,i=1正常处理(非重复层)

关键去重步骤:

  1. 第一层循环

    • 处理i=0(元素1)后,i=1(第一个2)正常处理
    • i=2(第二个2)时,i > start(0)nums[2]==nums[1],跳过,避免生成[2](同层重复)
  2. 第二层循环(处理元素1的子节点)

    • i=1(第一个2)时,start=1i == start,允许处理,生成[1,2]
    • i=2(第二个2)时,start=1i > start,但nums[2]==nums[1],是否跳过?此时i=2 > start=1,且值相同,跳过? 不,原代码中在第二层循环,start=1,i=1时处理第一个2,i=2时,i > start(1)为true,且nums[2]==nums[1],所以跳过。哦,这里之前模拟有误,正确逻辑是:在处理元素1的子节点(start=1)时,i从1开始,i=1处理第一个2,i=2处理第二个2时,因为i>start(1)且值相同,所以跳过。但这样会导致无法生成[1,2,2]吗?不,原代码中在start=1,i=1时处理第一个2,递归到start=2,此时处理第二个2是允许的,因为在子层中i=2等于start=2,不会触发跳过条件。

正确模拟:

  • 当处理start=1(元素1的子节点,即处理完1之后,处理后面的2),i=1(第一个2),此时start=1,i=1不大于start,所以允许处理,加入temp,递归到start=2,处理i=2(第二个2),此时i=2等于start=2,允许处理,生成[1,2,2]

五、去重条件的数学证明

命题:i > start && nums[i] == nums[i-1] 可消除同层重复子集

证明步骤:
  1. 排序保证相邻重复:排序后重复元素相邻,nums[i] == nums[i-1]仅当两者重复
  2. 同层与异层区分
    • 同层:i > start,说明i和i-1在同一层循环(同一个父节点的子节点),如父节点是1,子节点是第一个2和第二个2
    • 异层:i == start,说明i是当前层的第一个元素,即使与上层元素相同,属于不同路径(如父节点是2,子节点是另一个2)
  3. 重复子集的产生场景
    • 同层选择相邻重复元素会生成相同子集(如选择第一个2和第二个2在同层)
    • 异层选择重复元素属于不同路径(如父节点是1选择第一个2,父节点是2选择第二个2)
结论:

该条件确保在同一路径的同一层中,相同元素仅被处理一次,从而消除重复子集,同时保留不同层的合法选择。

六、算法复杂度分析

1. 时间复杂度

  • O(n × 2^n)
    • 子集总数为2^n(去重后最多2^n个子集)
    • 每个子集需O(n)时间复制到结果集
    • 排序时间O(n log n),总体为O(n × 2^n + n log n)

2. 空间复杂度

  • O(n)
    • 递归栈深度最大为n
    • temp列表长度最多为n
    • 结果集空间O(n × 2^n)

七、常见误区与优化建议

1. 忽略排序的重要性

  • 错误做法:未排序直接去重
    // 缺少Arrays.sort(nums)
    if (i > 0 && nums[i] == nums[i - 1]) continue; // 错误,无法保证同层重复
    
  • 后果:重复元素不相邻,条件判断失效,无法正确去重

2. 条件判断错误

  • 误区:使用i > 0代替i > start
    if (i > 0 && nums[i] == nums[i - 1]) continue; // 错误,会误杀异层重复
    
  • 正确逻辑:必须判断i > start,确保是同层重复而非异层

3. 优化建议:位运算去重

// 位运算解法(仅作示意,不推荐)
Set<List<Integer>> set = new HashSet<>();
for (int mask = 0; mask < (1 << n); mask++) {List<Integer> subset = new ArrayList<>();for (int i = 0; i < n; i++) {if ((mask & (1 << i)) != 0) {subset.add(nums[i]);}}set.add(subset);
}
res.addAll(set);
  • 劣势:利用集合去重,时间复杂度高,且无法利用排序优化

八、总结:同层去重的回溯优化本质

本算法通过排序和同层重复元素跳过策略,高效解决了含重复元素的子集去重问题,核心在于:

  1. 排序预处理:将重复元素相邻排列,为去重提供条件
  2. 同层重复检测:通过i > start && nums[i] == nums[i-1],避免同一层中选择重复元素
  3. 异层允许策略:不同层的重复元素允许选择,保证子集的完整性

理解这种去重逻辑的关键是区分同层与异层的重复元素:同层重复会导致相同子集,必须跳过;异层重复属于不同路径,允许存在。这种策略在组合、排列等含重复元素的问题中具有通用性,是回溯算法去重的经典解决方案。

相关文章:

  • Java后端优化:对象池模式解决高频ObjectMapper实例化问题及性能影响
  • 玩客云 OEC/OECT 笔记(2) 运行RKNN程序
  • 华为云Flexus+DeepSeek征文|利用华为云 Flexus 云服务一键部署 Dify 平台开发文本转语音助手全流程实践
  • py爬虫的话,selenium是不是能完全取代requests?
  • 【Day43】
  • 链式前向星图解
  • 06.MySQL数据库操作详解
  • Elasticsearch 读写流程深度解析
  • 相机--相机标定
  • mac安装brew时macos无法信任ruby的解决方法
  • Qt OpenGL 相机实现
  • 无他相机:专业摄影,触手可及
  • 排序算法C语言实现
  • flutter开发安卓APP适配不同尺寸的手机屏幕
  • FreeBSD 14.3 候选版本附带 Docker 镜像和关键修复
  • java28
  • SystemVerilog—new函数的使用和误区
  • 数据结构之堆:解析与应用
  • 数据结构哈希表总结
  • 高阶数据结构——并查集
  • c 做网站/服务营销策划方案
  • 做阿里云网站的公司吗/今日头条官方正版
  • 国外图片网站源码/友链查询站长工具
  • 义乌做网站要多少钱/免费的外链网站
  • 西安最新招聘信息今天/湖南seo优化报价
  • 网站建设费财务列账/聊城seo培训