题目是力扣第78题子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
提示:
1 <= nums.length <= 10
-10 <= nums[i] <= 10
nums 中的所有元素 互不相同
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/subsets
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
public class Solution {
public IList<IList<int>> Subsets(int[] nums) {
}
题解到处都有,也不是这篇文章的重点,这篇文章更想要带领读者以递归解题为目的思考如何一步步完成自己的递归函数
对于这个题目,他的结果特点:
子集都包含空集,所以我们计划在主函数直接向结果添加空集
其他的一眼看不出来,那只好先拿一个例子来分析,这里选用1,2,3,4,5
对于1,2,3,4,5这个数组,我们必须要思考他的结果是怎么来的,谁都可以直接看出结果比如子集有{1},{1,2},{1,2,3}等等,但如果你想设计的是一个程序,你必须找到一个方法并依次得到你的结果。
特别是对于递归的题解方式,我推荐大家在思考时只需要考虑我这个递归函数每一轮需要什么输入来得出我需要的结果。对于这个题,我们想要查找子集,那么我想要这样一个解题过程,对于一个选定的元素,查找到包含他所有的子集并加入结果中,采用这种策略不仅可以逐渐减少未处理元素地数量,也为了能让我自己在debug时容易查找遗漏结果。(你想啊,如果你可以逐个查看每一个元素的子集是否有遗漏,是不是会对你的debug有很大的帮助)
确定了思考方向,我们就得想想怎么设计(这里一定要想明白)
比如我这个元素确定是1,我怎么不重复地拿到所有包含他的结果,我们都直到递归讲究一个化繁为简最后得出结果,就是说:如果【1,2,3,4,5】选中1确实不好处理,但如果是【1】里选中1,那么很简单,子集就是【1】,你是不是想说这不是废话吗?嘿嘿还真是废话,重要的是这就是说,如果有一个递归他的参数最后可以变化到【单元素】这种形式,那么他就是可解的。
所以【1,2,3,4,5】当我们选中1时,可以把后面的部分当作一个元素传给递归函数,构成【元素1,元素2】的条件,也就是【【1】,【2,3,4,5】】,递归下去的函数中元素会越来越少,即这样递归的函数一定是可解的。当然不一定是所有的部分,我们选中1也就是想要找元素1存在的所有的子集,那么要包括后面所有的解,也就是说要包括【2,3,4,5】在分别选中2、3、4、5时的所有解,这句话不就等同于这几个递归吗
【【2】,【3,4,5】】
【【3】,【4,5】】
【【4】,【5】】
【【5】,【】】
因为已经选中了那个元素,所以后面的子集一定不包含这个元素即越来越短,如果后面依旧包含这个元素就会出现结果中重复元素的情况(这里应该不难理解,思考一下)
那么我们可以简单开始设计递归函数了,在设计递归函数时最好不要考虑主函数要如何调用等等琐事,你要关注和思考的一定是我这个递归函数要接收什么样的参数才能打到我的递归目的,也就是削减要处理的元素数量。
首先在主函数中排序数组(好习惯)并把通解空集添加给结果,然后先不管主函数后面怎么写的先去设计递归函数
这个递归函数名随便起DoItForMe
首先我们要返回结果,我这里是引用传入,你也可以复制传入最后返回。所以这个参数就出现了List<IList<int>> lili
对于这样的参数【【1】,【2,3,4,5】】,我们需要两个参数,一个用于接收选中元素在数组中的下标int iidx,一个用于接收后面的这个数组int[] resnums
所以目前我们的函数设计为这个样子
public class Solution
{
public IList<IList<int>> Subsets(int[] nums)
{
Array.Sort(nums);
List<IList<int>> lili = new List<IList<int>>();
lili.Add(new List<int>());
}
public void DoItForMe(int iidx, List<IList<int>> lili, int[] resnums,int[] resnums)
{
}
}
递归函数的内容设计思路,一般是判断当前的状态,如果还能继续递归则接着递归,如果不能直接返回。
所以什么时候不需要递归了呢?我们目前的传入有下标和剩余数组,当你的下标已经到最后一个了,也就是选中了剩余数组中唯一一个元素的情况(这正是我们上面所讲的废话)这时传入的剩余数组为空,也就是【元素】递归传入参数为【【元素】【】】的情况,那么这个元素就是他的解,所以我们直接把这个元素加入到上次递归出来的结果中。
可能这里有人会疑惑,他是结果的话直接加入到结果不就好了吗,跟上次递归的结果有什么关系??
还记得我们是如何逐渐削减元素到达这里的吗,我们不断选择一个元素并把他后面的所有元素看作一个元素才到达了这里,所以这个结果是在选择了这么多个元素的情况下才产生的,而在最后这个元素也是被选择,所以带上所有之前选择的元素才是真正的结果。
这时候发现参数还没有上次递归的结果(选中列表),哈哈,没事现加就行List<int> li
现在的思路函数设计为这样:
public class Solution
{
public IList<IList<int>> Subsets(int[] nums)
{
Array.Sort(nums);
List<IList<int>> lili = new List<IList<int>>();
lili.Add(new List<int>());
}
public void DoItForMe(int iidx, List<IList<int>> lili, int[] resnums,int[] resnums, List<int> li)
{
// 这里||左右的选一个即可,都写也行,一样的
if (iidx == nums.Length - 1 || resnums.Length == 0)
{
li.Add(nums[nums.Length - 1]);
lili.Add(li);
return;
}
}
}
再往下就是没到单元素的情况下,如何处理了
我们每次传入的参数iidx就是选中元素的下标,首先要把每次选中的元素加入选中列表,因为我们要找的是子集,即每加入一个元素都是一个全新的结果,所以不要忘记加入到结果列表中。
public class Solution
{
public IList<IList<int>> Subsets(int[] nums)
{
Array.Sort(nums);
List<IList<int>> lili = new List<IList<int>>();
lili.Add(new List<int>());
}
public void DoItForMe(int iidx, List<IList<int>> lili, int[] resnums,int[] resnums, List<int> li)
{
if (iidx == nums.Length - 1 || resnums.Length == 0)
{
li.Add(nums[nums.Length - 1]);
lili.Add(li);
return;
}
li.Add(nums[iidx]);
lili.Add(li);
}
}
还记得我们的递归逻辑吗,忘了没关系,我复制过来了,不用翻回去,复习一下
确定了思考方向,我们就得想想怎么设计(这里一定要想明白)
比如我这个元素确定是1,我怎么不重复地拿到所有包含他的结果,我们都直到递归讲究一个化繁为简最后得出结果,就是说:如果【1,2,3,4,5】选中1确实不好处理,但如果是【1】里选中1,那么很简单,子集就是【1】,你是不是想说这不是废话吗?嘿嘿还真是废话,重要的是这就是说,如果有一个递归他的参数最后可以变化到【单元素】这种形式,那么他就是可解的。
所以【1,2,3,4,5】当我们选中1时,可以把后面的部分当作一个元素传给递归函数,构成【元素1,元素2】的条件,也就是【【1】,【2,3,4,5】】,递归下去的函数中元素会越来越少,即这样递归的函数一定是可解的。当然不一定是所有的部分,我们选中1也就是想要找元素1存在的所有的子集,那么要包括后面所有的解,也就是说要包括【2,3,4,5】在分别选中2、3、4、5时的所有解,这句话不就等同于这几个递归吗
【【2】,【3,4,5】】
【【3】,【4,5】】
【【4】,【5】】
【【5】,【】】
因为已经选中了那个元素,所以后面的子集一定不包含这个元素即越来越短,如果后面依旧包含这个元素就会出现结果中重复元素的情况(这里应该不难理解,思考一下)
回到我们的递归函数,现在已经设置了结束条件,添加了每次选中的结果,就差最关键的递归了。
总结就是如果你想表示【1,2,3,4,5】在选中1时的所有结果,你必须要递归四次且此参数分别是:
【【2】,【3,4,5】】
计算选中1时又选中2的所有结果
【【3】,【4,5】】
计算选中1时又选中3的所有结果
【【4】,【5】】
计算选中1时又选中4的所有结果
【【5】,【】】
计算选中1时又选中5的所有结果
那么我们的递归部分就可以按此逻辑写出
public class Solution
{
public IList<IList<int>> Subsets(int[] nums)
{
Array.Sort(nums);
List<IList<int>> lili = new List<IList<int>>();
lili.Add(new List<int>());
}
public void DoItForMe(int iidx, List<IList<int>> lili, int[] resnums,int[] resnums, List<int> li)
{
if (iidx == nums.Length - 1 || resnums.Length == 0)
{
li.Add(nums[nums.Length - 1]);
lili.Add(li);
return;
}
li.Add(nums[iidx]);
lili.Add(li);
for (int newi = iidx + 1; newi < nums.Length; newi++)
{
int[] temp = new int[nums.Length - newi - 1];
Array.ConstrainedCopy(nums, newi + 1, temp, 0, nums.Length - newi - 1);
// 上面两行就是复制数组后面的一部分到temp里,随着newi变化复制的区间
DoItForMe(newi, nums, lili, temp, new List<int>(li));
// 注意li是复制传入,如果引用传入会影响其他递归的结果。
}
}
}
力扣运行结果