Problem overview
The method subsets receives an int[] called nums of distinct integers and returns every subset of it: every way to pick some of the numbers, from none (the empty subset) to all of them. Each subset is written in the order its numbers have in nums.
The order of the subsets matters too. The lesson decides about the numbers one at a time and always tries skipping before taking. Every subset without nums[0] comes before every subset with it, and inside each group the same rule applies to nums[1], then nums[2], and so on. For nums = [1, 2, 3] the result is:
[[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]
The first four subsets skip 1 and the last four take it. For [0] the result is [[], [0]]. For [3, -1] it is [[], [-1], [3], [3, -1]]: the last subset keeps the order of nums and is not sorted.
Approach: one choice per number
Each number has two possible fates: it is skipped or it is taken. With n numbers that is 2 to the power n combinations, and each combination is one subset. That is also the number of results, so any solution does at least that much work.
The method that generates them is backtracking. Walk through nums by index and keep the numbers taken so far in a list called current. At index i:
- Skip
nums[i]: move on to indexi + 1withcurrentunchanged. - Take
nums[i]: add it tocurrent, move on to indexi + 1, then remove it again.
When the index reaches the end of nums, every number has been decided, and current is a finished subset. Trying skip before take produces exactly the order the statement asks for. It is the same order as counting in binary: a subset is a string of n bits, 0 for skip and 1 for take.
The removal in step 2 is what makes the idea work. One list is shared by all the calls. After the "take" branch returns, the number is removed, so the caller sees current exactly as it was before the call and can go on from there. Undoing a choice is the "back" in backtracking.
Step by step
- Create
resultas aList<List<Integer>>holding a newArrayList<>(). Each subset is a list of numbers, so the result is a list of lists. - Start the search with
backtrack(nums, 0, new ArrayList<>(), result): index 0, nothing taken yet. The helper adds toresultevery subset that extends the one it receives. - Return
result. The helper filled this same list. - Write the helper.
backtrackreturnsvoidand takesnums, the indexito decide about now, the listcurrentandresult. - Stop when
i == nums.length. Every number has been skipped or taken. - Save a copy. Inside that
if, addnew ArrayList<>(current)toresultand return. A copy is needed becausecurrentkeeps changing as the search goes on. - Skip first. Call
backtrack(nums, i + 1, current, result)without touchingcurrent. This finds every subset withoutnums[i]. - Take. Add
nums[i]to the end ofcurrent. Numbers are added in index order, so every subset keeps the order ofnums. - Recurse again with the same arguments,
i + 1included. This timecurrentholdsnums[i], so this call finds every subset that contains it. - Undo. Remove the last number with
current.remove(current.size() - 1).
The solution in Java
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), result);
return result;
}
void backtrack(int[] nums, int i, List<Integer> current, List<List<Integer>> result) {
if (i == nums.length) {
result.add(new ArrayList<>(current));
return;
}
backtrack(nums, i + 1, current, result);
current.add(nums[i]);
backtrack(nums, i + 1, current, result);
current.remove(current.size() - 1);
}
}
Java details worth a closer look:
- Copy before saving.
result.add(current)would store a reference to the one shared list, and later changes would show in every saved entry. In the end each entry would be the same list, empty after the final undo. Written that way, the call on[1, 2, 3]returns[[], [], [], [], [], [], [], []]: eight entries, all empty.new ArrayList<>(current)copies the numbers into a new list. remove(int)versusremove(Object). On aList<Integer>, an argument of typeintremoves the element at that index, as incurrent.remove(current.size() - 1). To remove a value you would writeremove(Integer.valueOf(7)). With[5, 1, 7],remove(1)leaves[5, 7], because it removed the element at index 1, not the number 1.- Autoboxing.
current.add(nums[i])turns theintinto anIntegerautomatically. - The diamond.
new ArrayList<>()lets the compiler infer the element type from the declared type.
Complexity
Time: O(n * 2^n). There are 2^n subsets, and copying one takes up to n steps. The recursion makes about twice as many calls as there are subsets, which doesn't change the total.
Space: O(n) besides the answer. The helper holds current, which has at most n numbers, and one call per index, so the recursion is at most n calls deep.
Edge cases and common mistakes
- One number.
[0]gives[[], [0]]: two subsets, without it and then with it. - The empty subset is always there, and always first. The skip branch is followed all the way down before anything is taken.
- The size. With 10 numbers, the largest input allowed, the method returns
1024subsets. - Saving
currentwithout copying. It gives lists of empty subsets, as shown above. - Forgetting the undo. The taken number stays in
currentfor the rest of the search, and later subsets contain numbers that were never chosen for them. - Taking before skipping. The subsets are all there, but in a different order than the tests expect.
Testing
nums = [1, 2, 3]gives[[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]].nums = [0]gives[[], [0]].nums = [3, -1]gives[[], [-1], [3], [3, -1]].
