Problem overview
The method subsets(nums) receives an array of distinct integers and returns every subset of it: every way to pick some of its numbers, from none (the empty subset) up to all of them. The result is an array of arrays.
The numbers of each subset keep their order from nums, and the subsets themselves come in a fixed order, the one you get by deciding about the numbers one at a time, skipping before taking. Every subset without nums[0] comes before every subset with it. Inside each of those two groups, the subsets without nums[1] come first, then the ones with it, and so on. The empty subset is always first and the whole array always last.
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. In [[],[0]], the answer for [0], there are two subsets: without the number, then with it. For [3, -1] the result is [[],[-1],[3],[3,-1]]: the last subset is written [3,-1], in the order of nums, not sorted.
Approach: skip or take, one number at a time
Each number is either in a subset or not, so a subset is a string of n yes-or-no choices, and there are 2^n subsets. Backtracking walks through all the choices with one recursive function.
The function works on one index i of nums at a time and keeps the numbers taken so far in a list current. At index i it makes two choices, in this order:
- Skip
nums[i]: go on to indexi + 1withcurrentas it is. - Take
nums[i]: add it tocurrent, go on to indexi + 1, then remove it again.
When i reaches the end of nums, every number has been skipped or taken, and current is one complete subset: save a copy of it. Removing the number after the take branch is the "back" in backtracking. It restores current to what the caller handed in, so the caller can go on as if nothing happened.
Trying skip before take gives exactly the required order. All the subsets without nums[0] are produced before the first one with it, and the same holds at every level. Here are the first events of the search for [1, 2, 3]:
i=0 skip 1
i=1 skip 2
i=2 skip 3
save []
i=2 take 3 [3]
save [3]
i=1 take 2 [2]
i=2 skip 3
save [2]
i=2 take 3 [2,3]
save [2,3]
i=0 take 1 [1]
The first subset saved is the empty one, then [3], [2] and [2,3], and after that the search takes 1 and produces the other four.
Step by step
- Create
resultas a constant empty array. It collects the subsets. - Start the search with
this.backtrack(nums, 0, [], result): index 0 is the first number to decide about, and the subset being built is empty. - Return
result. The helper added to this same array. - Declare the helper
backtrack(nums, i, current, result)inside the class. Each call decides aboutnums[i]and calls itself for the next index. - Check whether
ihas reached the end:i === nums.length. - Save a copy and return.
[...current]copies the numbers into a new array.currentkeeps changing, so the result needs a snapshot. - Skip first: call
this.backtrack(nums, i + 1, current, result)without touchingcurrent. This finds every subset that doesn't containnums[i]. - Take: push
nums[i]onto the end ofcurrent. Numbers are added in index order, so each subset keeps the order ofnums. - Recurse again with the same arguments. This time
currentholdsnums[i], so this call finds every subset that contains it. Together with the skip call, each subset is found exactly once. - Undo the choice with
current.pop(), which removes the last number.
The solution in JavaScript
class Solution {
subsets(nums) {
const result = [];
this.backtrack(nums, 0, [], result);
return result;
}
backtrack(nums, i, current, result) {
if (i === nums.length) {
result.push([...current]);
return;
}
this.backtrack(nums, i + 1, current, result);
current.push(nums[i]);
this.backtrack(nums, i + 1, current, result);
current.pop();
}
}
The JavaScript details worth a closer look:
- The copy matters. Arrays are passed by reference, so every call works on the same
current. If the code pushedcurrentitself,resultwould hold several references to one array, and at the end that array is empty again. A version without the copy returns[[],[],[],[]]for[7, 8], instead of[[],[8],[7],[7,8]]. [...current]is the spread syntax: it builds a new array with the same elements. It is a shallow copy, which is enough because the elements are numbers.constforresult.conststops the variable from being reassigned, but the array it names can still grow. That is whyresult.push(...)works on a constant.- Methods are called through
this.backtrackbelongs to the class, so the calls arethis.backtrack(...). Withoutthis., JavaScript looks for a variable namedbacktrackand throws aReferenceError. ===for the index check. Both sides are numbers, and===compares them without converting types.
Complexity
Time: O(n · 2^n). There are 2^n subsets, and copying one takes up to n steps. The recursion makes about 2^(n+1) calls in total, which is within the same bound.
Space: O(n) beyond the answer. The extra memory is current, which holds at most n numbers, and the recursion, which is n + 1 calls deep. The answer itself holds up to n · 2^n numbers. With n up to 10, that is at most 1,024 subsets.
Edge cases and common mistakes
- One number.
[0]returns[[],[0]]: the skip branch saves the empty subset, and the take branch saves[0]. - Two numbers.
[5, 6]returns[[],[6],[5],[5,6]]. - Negative numbers and order.
[3, -1]returns[[],[-1],[3],[3,-1]]. The code never sorts or compares values, so the subsets follow the order ofnums. - The count. For
nnumbers the result holds2^nsubsets, including the empty one. For ten numbers that is 1,024. - Forgetting the copy. The result then holds the same array several times, which is empty at the end.
- Forgetting
pop().currentkeeps numbers that should have been removed, so later subsets contain numbers they shouldn't. - Taking before skipping. The subsets are all there, but in a different order, and the lesson's tests expect skip-first order.
- Saving at every call. Only the calls at
i === nums.lengthsave. Saving earlier would store the same subset many times.
Testing
[1, 2, 3]gives[[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]].[0]gives[[],[0]].[3, -1]gives[[],[-1],[3],[3,-1]].[5, 6]gives[[],[6],[5],[5,6]].[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]returns 1,024 subsets.
