题目:
Given a set of distinct integers, nums, return all possible subsets (the power set).
Note: The solution set must not contain duplicate subsets.
Example:
Input: nums = [1,2,3]
Output:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
这道题是上周V家CodeHouse OA的题目之一变种,当时真是急得没法冷静思考了,今天扫了下solution的思路然后自己想了一下就磕磕绊绊写出来了,感觉其实还是挺简单的。
如果是拿张纸自己做这道题,我会按长度来排,这样我生成的list of list就是:1, 2, 3, | 12, 13, 23, | 123,但是写起代码比较复杂,放到后面讲。
这道题最简单的做法并不是按长度生成,而是:每次在已生成的序列中加入一个新的数字。这样生成的序列就是:1 | 2 12 | 3 13 23 123,也就是第一次加入了1,第二次在之前生成好的序列中加入2,第三次再在之前生成好的序列中加入3。但是这里还有一个问题,就是怎样生成长度为1的序列(就是只加一个数字),这时候就需要来一个dummy空序列,每次要生成长度为1的时候就是加入这个空序列。代码写起来也算比较简单。时间复杂度外层循环nums是O(n),内层循环result,由于最后生成的结果集的size是2^n,所以内层相当于是2、4、8、...、2^n这样的循环次数,也就还是O(2^n),所以整体时间复杂度是O(n* 2^n)。空间复杂度也是如此。
Runtime: 1 ms, faster than 58.34% of Java online submissions for Subsets.
Memory Usage: 39.8 MB, less than 47.83% of Java online submissions for Subsets.
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new LinkedList<>();
result.add(new LinkedList<>());
for (int i = 0; i < nums.length; i++) {
int size = result.size();
for (int j = 0; j < size; j++) {
List<Integer> subset = new LinkedList<>(result.get(j));
subset.add(nums[i]);
result.add(subset);
}
}
return result;
}
}
回到按长度来生成list的方法,这种思想转换成代码需要写三重循环,第一层是对长度进行遍历,在每个长度之内,我们采用上一个长度生成出来的list of list(比如长度为1的list of list就是1,2,3),每次对一个上个长度的list进行操作(第二层循环),这个操作就是在这个list后面加更大的数字(比如对12,后面就只加3),这个操作就是第三层循环(遍历所有的数字,选择比list中末尾数字更大的数字加入list)。写成代码涉及到一些临时list of list的操作,可能有点绕,但整体思路还是比较清晰的。还有就是最开始要生成长度为1的list时,需要前面先有一个空list作为baseline,往这里面加数字。这种做法的时间复杂度我不太确定,感觉外层是O(n),第二层不太确定,我猜是是O(),第三层又是O(n),所以总的是
Runtime: 3 ms, faster than 7.14% of Java online submissions for Subsets.
Memory Usage: 40.2 MB, less than 16.23% of Java online submissions for Subsets.
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new LinkedList<>();
List<List<Integer>> temp = new LinkedList<>();
result.add(new LinkedList<>());
temp.add(new LinkedList<>());
// length of the output array
for (int i = 0; i < nums.length; i++) {
// store the list of a specific length
List<List<Integer>> newTemp = new LinkedList<>();
// traverse result from last time
for (List<Integer> list : temp) {
// add new numbers into the result from last time
for (int j = 0; j < nums.length; j++) {
// only add the numbers in the first iteration or if they're larger
if (list.size() == 0 || list.get(list.size() - 1) < nums[j]) {
List<Integer> newList = new LinkedList<>(list);
newList.add(nums[j]);
result.add(newList);
newTemp.add(newList);
}
}
}
temp = newTemp;
}
return result;
}
}
最后一种方法是backtracking。如果按照长度来backtracking的话比较冗余,感觉没有必要,LC solution就是长度backtracking,print出来感觉多了n倍的工作量。大概先讲一下backtracking的思路,就是先加入一个数字1,然后在1的后面加入一个可选的(比如2),2的后面再加入一个可选的也就是3,加入3以后发现没有更多的了,就把3移除回到2,发现2后面也没得可加的了,就回到1,1后面接着可以接3,对3也是同样对2一样的操作。LC上solution的图感觉很直观,借用一下:
如果按照LC solution的按长度backtracking的方法,每次只生成一个长度的序列,需要记录这个长度。并且每次在生成的时候也要经历1->12->123->2->....才能加入2,就真的过于复杂了。贴个代码在这里,不做详细研究了。和普通backtrakcing相比冗余的部分用注释标了出来,也可以通过欣赏print出来的log来发现这真的做了很多无用功。
class Solution {
public void backtrack(List<List<Integer>> list, List<Integer> temp,
int[] nums, int start, int length) { // no need for 'length'
System.out.println(list + ", " + temp + ", " + start + ", " + length);
// if (temp.size() == length) {
list.add(new LinkedList<>(temp));
// }
for (int i = start; i < nums.length; i++) {
temp.add(nums[i]);
backtrack(list, temp, nums, i + 1, length);
temp.remove(temp.size() - 1);
}
}
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new LinkedList<>();
// for (int i = 0; i < nums.length + 1; i++) {
backtrack(result, new ArrayList<>(), nums, 0, i);
// }
return result;
}
}
/*
[], [], 0, 0
[[]], [1], 1, 0
[[]], [1, 2], 2, 0
[[]], [1, 2, 3], 3, 0
[[]], [1, 3], 3, 0
[[]], [2], 2, 0
[[]], [2, 3], 3, 0
[[]], [3], 3, 0
[[]], [], 0, 1
[[]], [1], 1, 1
[[], [1]], [1, 2], 2, 1
[[], [1]], [1, 2, 3], 3, 1
[[], [1]], [1, 3], 3, 1
[[], [1]], [2], 2, 1
[[], [1], [2]], [2, 3], 3, 1
[[], [1], [2]], [3], 3, 1
[[], [1], [2], [3]], [], 0, 2
[[], [1], [2], [3]], [1], 1, 2
[[], [1], [2], [3]], [1, 2], 2, 2
[[], [1], [2], [3], [1, 2]], [1, 2, 3], 3, 2
[[], [1], [2], [3], [1, 2]], [1, 3], 3, 2
[[], [1], [2], [3], [1, 2], [1, 3]], [2], 2, 2
[[], [1], [2], [3], [1, 2], [1, 3]], [2, 3], 3, 2
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3]], [3], 3, 2
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3]], [], 0, 3
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3]], [1], 1, 3
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3]], [1, 2], 2, 3
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3]], [1, 2, 3], 3, 3
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]], [1, 3], 3, 3
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]], [2], 2, 3
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]], [2, 3], 3, 3
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]], [3], 3, 3
*/
接下来谈谈优化的方法,就是直接按照开头数字来生成:1->12->123->13->2->23这样。我们的backtrack函数需要记录的内容有:结果集result,一个用于存放当前backtrack的中间结果的temp,还有最原始的输入nums,和当前backtrack的start index。在backtrack函数中,每次进入这个函数,就将当前的中间结果temp生成的序列加入到result中(毕竟它也是个possible outcome),接下来就要对着剩下还没遍历到的部分(nums[start]及其以后)进行遍历,在遍历的过程中,先把当前的数字加入temp,再调用backtrack函数等着它接着遍历,等它遍历完以后就要把刚刚加入到temp里的给remove掉,因为已经对它backtrack完了,等着遍历下一个可能的数字。所以代码如下:
Runtime: 1 ms, faster than 58.00% of Java online submissions for Subsets.
Memory Usage: 39.9 MB, less than 31.13% of Java online submissions for Subsets.
class Solution {
public void backtrack(List<List<Integer>> result, List<Integer> temp,
int[] nums, int start) {
System.out.println(result + ", " + temp + ", " + start);
result.add(new LinkedList<>(temp));
for (int i = start; i < nums.length; i++) {
temp.add(nums[i]);
backtrack(result, temp, nums, i + 1);
temp.remove(temp.size() - 1);
}
}
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new LinkedList<>();
backtrack(result, new ArrayList<>(), nums, 0);
return result;
}
}
/*
[], [], 0
[[]], [1], 1
[[], [1]], [1, 2], 2
[[], [1], [1, 2]], [1, 2, 3], 3
remove: 3
remove: 2
[[], [1], [1, 2], [1, 2, 3]], [1, 3], 3
remove: 3
remove: 1
[[], [1], [1, 2], [1, 2, 3], [1, 3]], [2], 2
[[], [1], [1, 2], [1, 2, 3], [1, 3], [2]], [2, 3], 3
remove: 3
remove: 2
[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3]], [3], 3
remove: 3
*/
还有一种是用到了bit mask的思想,目前觉得有点tricky不是常规做法。大概思想就是可以把这个问题想象成这一串数字和一个与它长度相等的二进制bit mask“相与”,如果对应位置的mask为1就要它,为0就不要它。只要能生成所有的bitmask(000,001, 010, 011, 100, 101, 110, 111),就可以对nums遍历来取对应位置的组合。不想仔细研究了,就直接贴solutions的代码了:
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> output = new ArrayList();
int n = nums.length;
for (int i = (int)Math.pow(2, n); i < (int)Math.pow(2, n + 1); ++i) {
// generate bitmask, from 0..00 to 1..11
String bitmask = Integer.toBinaryString(i).substring(1);
// append subset corresponding to that bitmask
List<Integer> curr = new ArrayList();
for (int j = 0; j < n; ++j) {
if (bitmask.charAt(j) == '1') curr.add(nums[j]);
}
output.add(curr);
}
return output;
}
}
所有方法的时空复杂度都是O(n* 2^n)。