本文介绍给定数组求子集算法,注意是数组而不是集合,这意味着数组中可能有重复元素。因此本文会分别介绍无重复数字(集合)求子集,和有重复数字求子集。其他算法链接如下:
1 无重复数字求子集(集合的子集)
leetcode实战:子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
1.1 二进制查找
一个长度为N的集合的子集个数为2N,一个长度为N的二进制序列可能产生2N种结果,因此可以很容易想到将它们之间建立起一一对应的关系:二进制序列中的每一位值可以取1或0,代表取或不取集合中相应位置的元素来组成子集,从而通过枚举二进制序列来得到所有的子集。
我们将与集合set长度等长的二进制序列bitarray中的每一位值定义如下:若bitarray[i]=1,则表示set[i]被选中进当前子集中,反之则未被选中。以下图为例:
集合set={1,2,3,4,5,6},二进制序列bitarray与它等长,它的每一位都和集合中的每个元素一一对应。对于序列①=010010,即bitarray[2]=bitarray[5]=1,它表示将set[2]和set[5]选中,那么序列①代表的子集为{2,5}。同理序列②代表子集{1,2,3,4,5}。
因此,我们只要将所有的二进制序列枚举出来,就可以获得所有子集。以vis
存储序列,使用函数findsubset(cur)
表示对vis[cur]
进行0,1枚举,以表示是否选中set[i]
进入子集中。首先对vis[cur]
置1,然后执行findsubset(cur+1)
去枚举下一个bit的值,当递归结束返回到此时,再对vis[cur]
置0,再继续递归findsubset(cur+1)
。当递归函数执行到边界条件cur==lens
时(lens为集合长度),说明已经得到一个二进制序列,然后根据序列中的1的位置选中相应元素即可得到一个子集,当所有函数都递归结束时候,就找到了所有的子集。
代码如下:
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
lens=len(nums)
vis=[0 for i in range(lens)] #二进制序列,0:未选中 1:选中
ans=[]
def findsubset(cur):
if(cur==lens):
tmp=[]
for i in range(lens):
if vis[i]: #根据flag指示查看当前那些元素被包含在子集中
tmp.append(nums[i])
ans.append(tmp)
return
vis[cur]=1 #当前位选定
findsubset(cur+1)
vis[cur]=0 #不选当前位
findsubset(cur+1)
findsubset(0)
return ans
上述方法是通过选中或不选中集合里的每一个元素来确定一个子集,并使用二进制序列辅助寻找,这样能更好的理解。理解这种方法之后其实可以不再需要vis
数组,只要在形参上设置一个lst
参数来保存当前子集的元素即可。下面的代码与上述二进制序列方法不同的是选择子集的方式,findsub(cur,lst)
执行的指令是将set[cur]
先加入子集lst
,再将它从lst
中删除,递归的地方不变,它们表示的含义相同,都是对每一个元素,执行“选”与“不选”,来得到所有的子集。
代码如下:
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
lens=len(nums)
ans=[]
def findsub(cur,lst=[]):
if(cur==lens):
ans.append([num for num in lst])
return
lst.append(nums[cur])
findsub(cur+1)
lst.pop()
findsub(cur+1)
findsub(0)
return ans
1.2 快速选择法
1.1中的方法需要在对所有元素执行“选与不选”后才能确定一个子集,即每次都需要到cur==lens
时才产生一个子集并存入。事实上,我们在“挑选”的过程中,就在不断产生子集,无需对所有元素挑完才确定一个子集。
以下图为例:
第一次我们选择1,这样自然得到了一个子集{1},然后我们会从1后面的数字开始选,比如2,那么又得到子集{1,2}…
当我们挑选到{1,2,3,4}时,就已经得到了4个子集,而通过1.1的方法,才确认一个子集。到④之后,我们退回到第三步(也就是第三次递归),可以选择数字3后面的数字,也就是4,然后得到一个子集{1,2,4}…
从上述流程可以看出,快速选择法与二进制查找的不同点是:二进制查找的函数只负责将当前位cur
,也就是nums[cur]
包含或不包含在子集中,而快速选择法能够在当前递归中选择将nums[cur]
以及它后面的数字包含或不包含在子集中。
在快速选择法的递归函数findsub(cur)
中,我们会依次选择nums[cur]~nums[lens-1]
,每次选择一个数字(下标为i
)后,就进行下一次递归findsub(i+1)
从而避免重复。在每次递归时,都是一次选择的过程,因此每次递归都需要将当前选中的数字组成集合加入到子集集合中去。
具体代码如下:
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
lens=len(nums)
ans=[]
def findsub(cur,lst=[]):
ans.append([num for num in lst])
for i in range(cur,lens):
lst.append(nums[i])
findsub(i+1)
lst.pop()
findsub(0)
return ans
2 有重复数字求子集
leetcode实战:子集II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
当元素出现重复的时候,求解方法与全排列算法中的重复问题类似。
对于nums={1,2,2,3}来说,子集{1,2,3}中的2选择nums中的第一个2或第二个2意义一样,也就是当我们已经在{1,2,3}中的第二位选择了nums中的第一个2之后,就没有必要再选择nums中的第二个2,所以当我们尝试将nums中的第二个2放在第二位的时候,应当先判断是否出现重复。为了高效判断是否重复,我们需要先对nums排序,然后在每次递归中选择数字之前通过nums[i]=?nums[i-1]
来判断:若nums[i]==nums[i-1]
则意味着当前位置的数字与它前面的那个数字一致,而在选择nums[i]之前,nums[i-1]就已经被选择过了,那么就没必要再次放一个相同的数字在当前位置上,因此直接跳过,但是要注意一个边界条件:i==cur
,也就是i是当前递归中首个能被选择的数字,不存在i-1位。综合这两个情况,选择数字的判断条件为:i==cur or nums[i]!=nums[i-1]
。
具体代码如下:
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort()
lens=len(nums)
ans=[]
def findsub(cur,lst=[]):
ans.append([i for i in lst])
for i in range(cur,lens):
if(i==cur or nums[i]!=nums[i-1]):
lst.append(nums[i])
findsub(i+1)
lst.pop()
findsub(0)
return ans