LeetCode 90 | 经典递归问题,求出所有不重复的子集II
本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是LeetCode专题第56篇文章,我们一起来看看LeetCode第90题,子集II(Subsets II)。
这题的官方难度是Medium,通过率46.8%,点赞1686,反对73。看得出来是一道偏基础,然后质量很高的题。既然有Subsets II自然有Subsets I,它的前作是78题,和78题相比,题意稍稍有些改动,如果没做过78题的,建议可以先看下,有个对比。
LeetCode 78,面试常用小技巧,通过二进制获得所有子集
题意
给定一个包含重复元素的数组,要求生成出这些元素能够构成的所有子集。注意,子集包括空集和全集。
在之前的LeetCode78题当中,给定用来生成子集的数组当中不包含重复的元素。这也是这两题当中最大的差别。
样例
Input: [1,2,2]
Output:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
题解
全排列的问题也好,获取子集也好,这些问题都已经算是老生常谈了,我们之前做过不少。这些问题经过转化之后,本质上还是搜索问题。我们在样本空间当中搜索所有合法的解,存储起来。
这道题的前身LeetCode78题用的正解也是搜索的解法,对于使用搜索算法来解这道题问题不大,但问题是针对数组当中的重复元素我们应该怎么样来处理。
最简单也是最容易想到的方法当然是先把所有的子集全部找到之后,我们再进行去重。如果采用这样的方法,还有一个便利是我们可以不用递归,而是可以通过二进制枚举的方法获取所有的子集。但也有一个问题,问题就是复杂度。我们把集合当中的每一个数字都看成是独立的,那么对于每一个数字来说都有取和不取两种方案。对于n个数字来说,方案总数当然就是。并且我们还需要对这个集合进行去重,这带来的开销可想而知。
当然针对这个问题我们也有解决方案比如可以用hash算法将一个集合hash成一个数,如果hash值一样说明集合的构成相同。这样我们就可以通过对数字去重来实现集合去重了。
但这样仍然不是完美的,首先hash算法也不是百分百可靠的,也可能会出现hash值碰撞的情况。其次,这种方案的实现复杂度也很大,我们找出所有集合之后再通过hash算法进行过滤,整个过程非常麻烦。
很明显,这题一定还存在更好的方法。
既然事后找补不靠谱,那么我们可以试着事前避免。也就是说我们在搜索所有子集的时候就设计一种机制可以过滤掉重复的集合或者是保证重复的集合不会出现。我们可以分析一下重复的集合出现的原因,两个集合完全一样,说明其中的元素构成完全一致。元素的构成一致又有两种可能,第一种是重复的获取,比如[1, 3],我们先拿1再拿3和先拿3再拿1本质上是一样的。还有一种可能是元素的重复导致的集合重复,比如[1, 3]假如我们候选的1不止一个,那么拿不同的1也会被认为是不同的方案。
针对第一种情况出现的重复非常简单,我们可以对元素进行排序,之后限定拿取元素的顺序。只能从左拿到右,不能先拿右边的元素再回头拿左边的元素,这样就禁止了第一种情况导致的重复。这个方法我们曾经在很多问题当中用到过,就不详细介绍了。
下面来说说第二种情况,就是重复元素导致的重复集合。这一点需要结合代码来仔细说明,我们来看一段经典的搜索代码:
def dfs(cur, subset):
for i in range(cur, n):
nxt = subset + [nums[i]]
ret.append(nxt)
dfs(i+1, nxt)
这一段是一个经典的搜索代码,我们在for循环当中执行的其实是一个枚举操作,也就是枚举这一轮我们要拿取哪一个元素。这里我们限制了选择的范围只能在上一次选择元素的右侧,也就是上文当中说的针对第一种情况的方案。假设我们当前候选的元素是[1, 1, 3, 3],这里虽然有4个元素,但是值得我们搜索的其实只有两个,就是1和3。因为第二个1和第二个3都没有任何用处,只会导致结果重复。
并且假设我们希望得到[1, 1]这样的结果,只能通过拿取左侧的1实现。也就是说如果出现重复的元素,我们只需要考虑第一个出现的,其余都没有考虑的必要。
为了更加形象, 我们画出这一段的搜索树。这里我们为了简化图示,只画了[1, 1, 3]三个数的情况。可以看出我们选第一个1和第二个1,都构建出了[1, 3]这个集合,这是重复的。并且我们可以发现第二个1的所有情况第一个1都已经包括了,所以这一整个分支都是多余的,可以剪掉。

最后,我们把上面的细节全部串起来写出代码:
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
# 对元素排序,将重复的元素挨在一起
nums = sorted(nums)
ret = [[]]
n = len(nums)
def dfs(cur, subset):
# 上一次选择的元素,一开始置为None
last = None
for i in range(cur, n):
if i == cur or nums[i] != last:
# 存储集合
nxt = subset + [nums[i]]
ret.append(nxt)
# 更新last
last = nums[i]
dfs(i+1, nxt)
dfs(0, [])
return ret
总结
到这里,我们关于这道题的介绍就结束了。从代码上来看,这道题的代码不长,涉及到需要推理的细节也并不多,总体的难度并不大。但作为一道搜索问题,它仍然非常有价值。如果你能自己思考推导得出正确的递归代码,那么说明你对递归的理解已经可以算是合格了,所以这题也非常适合面试,要准备找工作的小伙伴,可以仔细刷刷。
今天的文章到这里就结束了,如果喜欢本文的话,请来一波素质三连,给我一点支持吧(关注、转发、点赞)。
- END -
LeetCode 90 | 经典递归问题,求出所有不重复的子集II的更多相关文章
- AJPFX:不用递归巧妙求出1000的阶乘所有零和尾部零的个数
package com.jonkey.test; import java.math.BigInteger; public class Test6 { /*** @param args* 需求:求出1 ...
- [Leetcode 90]求含有重复数的子集 Subset II
[题目] Given a collection of integers that might contain duplicates, nums, return all possible subsets ...
- LeetCode 31:递归、回溯、八皇后、全排列一篇文章全讲清楚
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天我们讲的是LeetCode的31题,这是一道非常经典的问题,经常会在面试当中遇到.在今天的文章当中除了关于题目的分析和解答之外,我们还会 ...
- 从"汉诺塔"经典递归到JS递归函数
前言 参考<JavaScript语言精粹> 递归是一种强大的编程技术,他把一个问题分解为一组相似的子问题,每一问题都用一个寻常解去解决.递归函数就是会直接或者间接调用自身的一种函数,一般来 ...
- 72【leetcode】经典算法- Lowest Common Ancestor of a Binary Search Tree(lct of bst)
题目描述: 一个二叉搜索树,给定两个节点a,b,求最小的公共祖先 _______6______ / \ ___2__ ___8__ / \ / \ 0 _4 7 9 / \ 3 5 例如: 2,8 - ...
- LeetCode:子集 II【90】
LeetCode:子集 II[90] 题目描述 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集). 说明:解集不能包含重复的子集. 示例: 输入: [1,2,2] 输出: ...
- NYOJ90 整数划分(经典递归和dp)
整数划分 时间限制:3000 ms | 内存限制:65535 KB 难度:3 描述 将正整数n表示成一系列正整数之和:n=n1+n2+…+nk, 其中n1≥n2≥…≥nk≥1,k≥1. 正 ...
- [LeetCode] Excel Sheet Column Title 求Excel表列名称
Given a positive integer, return its corresponding column title as appear in an Excel sheet. For exa ...
- poj 2001:Shortest Prefixes(字典树,经典题,求最短唯一前缀)
Shortest Prefixes Time Limit: 1000MS Memory Limit: 30000K Total Submissions: 12731 Accepted: 544 ...
随机推荐
- xmake从入门到精通12:通过自定义脚本实现更灵活地配置
xmake是一个基于Lua的轻量级现代化c/c++的项目构建工具,主要特点是:语法简单易上手,提供更加可读的项目维护,实现跨平台行为一致的构建体验. 本文主要详细讲解下,如何通过添加自定义的脚本,在脚 ...
- jmeter压力测试报错:java.net.BindException: Address already in use: connect || java.net.SocketException: Socket closed
windows提供给TCP/IP链接的端口为 1024-5000,并且要四分钟来循环回收它们,就导致我们在短时间内跑大量的请求时将端口占满了,导致如上报错. 解决办法(在jmeter所在服务器操作): ...
- R常用统计 - 相关关系分析
数据格式 每行分别为表型和基因表达情况对应标量,每列分别为样品名的矩阵.假定前9列为phenotype,从第10行起为gene_id,编写简单for循环如下: script require(" ...
- R 数据读取与写入
路径 getwd() #获取当前工作路径 setwd() #设置工作路径 获取普通文本数据 x = read.table("data.txt") #通过路径直接获取 x = rea ...
- python线程,进程,队列和缓存
一.线程 threading用于提供线程相关的操作,线程是应用程序中工作的最小单元. 创建线程的两种方式1.threading.Thread import threading def f1(arg): ...
- MacOS 键盘符号和修饰键说明
原文链接:https://www.cnblogs.com/exmyth/p/5949192.html Mac键盘符号和修饰键说明 ⌘ Command ⇧ Shift ⌥ Option ⌃ Cont ...
- logging日志基础示例
import logging logger = logging.getLogger() # 获取日志对象 logfile = 'test.log' hdlr = logging.FileHandler ...
- Python 中 False 和 True 关键字
False:布尔类型,假.当条件判断不成立时,返回False. # == 判断两个对象的值是否相等 print('' == False)# False print(None == False)# Fa ...
- 预定义的 $_GET 变量用于收集来自 method="get" 的表单中的值
PHP $_GET 变量 在 PHP 中,预定义的 $_GET 变量用于收集来自 method="get" 的表单中的值. $_GET 变量 预定义的 $_GET 变量用于收集来自 ...
- 4.15 省选模拟赛 编码 trie树 前缀和优化建图 2-sat
好题 np. 对于20分 显然可以爆搜. 对于50分 可以发现每个字符串上的问号要么是0,要么是1.考虑枚举一个字符串当前是0还是1 这会和其他字符串产生矛盾. 所以容易 发现这是一个2-sat问题. ...