Given a set of distinct integers, nums, return all possible subsets.
Note:
- Elements in a subset must be in non-descending order.
- The solution set must not contain duplicate subsets.
For example,
If nums = [1,2,3]
, a solution is:
[ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
这是一道求一个给定集合所有子集的问题,其核心问题在于如何遍历整个子集。
对于这道题目我的思路是一个straightforward的方法即: 根据子集元素个数进行遍历(0~nums.size()),在每一种子集下面对固定长度(假设为n)的子集,我们可以固定前n-1个元素,激活第n个元素遍历,再固定前n-2个元素,后面两个激活进行遍历。。。。最后,当遍历完整个长度区间的时候我们就得到了该集合的所有子集。 无奈,得出子集的输出顺序与leetcode的测试用例不同,无奈过不了。不过正因为如此,我学习到了两种求子集的方法。
1、回溯法:
采用递归的方式实现
1 const int n = 4; 2 int x[n]; 3 //回溯法 4 void backtrack(int t) 5 { 6 if(t >= n) 7 { 8 for(int i = 0; i < n; i++) 9 cout<<x[i]; 10 cout<<endl; 11 } 12 else 13 { 14 for(int i = 0; i <= 1; i++) 15 { 16 x[t] = i;//针对每一个元素进行判断,1表示该元素存在于该子集中,0表示不存在 17 backtrack(t + 1); 18 } 19 } 20 }
通过对每一个元素的存在与否进行回溯,很容易就遍历了整个集合。
这段代码核心就是14~18行,我们可以看到当第一次进入时,即从主线程中调用 backtrack(0),程序会执行到14行,这里有两个分支,分别是x[0]=0;x[0]=1;按照程序执行的顺序来看不是并发的。不过在逻辑上是这么执行的。
好了现在到下一个判断(由于t<n,所以第一个if不生效),此时第一个分支执行又会派生出两个分支,分别是x[1]=0,x[1]=1,同样,第一个分支的另一部分也会派生出两个分支,于是我们得到以下组合{x[0],x[1]} = {0,0} or {0,1} or {1,0} or {1,1}。
此时很明显对前两个元素的所有组合做了一个遍历,按照这种结构继续递归,最后(t==n),我们将会对所有的元素子集做出一个遍历(其实就是利用了二叉树的结构,所有的叶子就是我们要的结果)。
2、位运算法
这个方法直接遍历整个子集,没有递归,故运算速度快于第一种。这是某位博主在topcoder上面看到的。我们就时以长技以自强咯。
整个遍历的思想是很简单的:
该算法包含两个循环:
第一重循环是枚举所有子集,共2^n个,即1 << n个
第二重循环求集合所有j个元素的值,0或1。
1 void bitOperate() 2 {//对所有子集遍历,每次求出一个子集 3 for(int i = 0; i < (1 << n); i++) 4 { 5 for(int j = 0; j < n; j++) 6 {//对第i个子集进行筛选 7 if( (i & (1 << j) ) == 0) 8 x[j] = 0; 9 else 10 x[j] = 1; 11 } 12 for(int j = 0; j < n; j++) 13 cout<<x[j]; 14 cout<<endl; 15 } 16 }
有很多朋友可能刚刚看到的这段代码回犯迷糊,其实这个为什么要叫位运算法呢?个人认为可能是用了位操作得名的吧。其实它的核心并非是利用了位运算而得到一个遍历的方法,位运算只是起到一个计算工具的作用,其核心还是利用"位"当了一个标志,
怎么说呢。一个大小为n的集合,其子集为2^n个(不懂的回去看看高中课本),正好我们可以利用二进制数来表示每一个子集(给它们起名字),集合与集合之间不同体现在元素,当元素为数字时,可以更加细化:集合之间不同在于元素的数量和元素的值。这两个正好对应二进制中1的位置。比如一个集合用8即(1000),这就表示第0号元素存在,其他元素不存在的集合。
了解了这些,就可以在内层中用下标j来进行循环求出下标为j的元素是否存在于这个子集中。保存存在的元素,那么就得到了这个子集了。
以上为大家介绍了了leetcode Subsets题目的核心算法,当然只是数学方法,具体编程的实现大家可以自己去试试!