Problem overview
The method subsets receives a list 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 keeps the order its numbers have in nums.
The order of the subsets matters too. The method decides about the numbers one at a time, trying "skip" before "take". Every subset without nums[0] comes before every subset with it. Inside each of those groups, the subsets without nums[1] come first, and so on. The empty subset is always first and all of nums is 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. For nums = [0] the result is [[], [0]]. For nums = [3, -1] it is [[], [-1], [3], [3, -1]]: the last subset keeps the order of nums, it isn't sorted.
Approach: one decision per number
Each number is either in a subset or not, so building a subset is a series of yes/no decisions, one per number. With n numbers there are 2^n series of decisions, which is the number of subsets. For 10 numbers, that is 1024, and the method does return 1024 subsets for nums = [0, 1, ..., 9].
Backtracking walks through all those decision series. Keep a list current with the numbers taken so far, and a position i that says which number to decide about next:
- If
iis past the last number, every number has been decided.currentis a finished subset, so save it. - Otherwise, first skip
nums[i]: continue withi + 1and leavecurrentas it is. - Then take
nums[i]: add it tocurrent, continue withi + 1, and afterwards remove it again.
The removal is the "back" in backtracking. When the call that took nums[i] returns, current must be exactly what the call received, so the caller can go on with its own next choice. Trying skip before take produces the order the statement asks for: the branch that skips nums[0] is explored completely before the branch that takes it, and the same holds at every deeper level.
The same order comes from counting in binary. Write 0 for skip and 1 for take: the subsets appear in the order of the bit strings 000, 001, 010, and so on.
Step by step
- Create
resultas an empty list. It collects the subsets, so it will be a list of lists. - Start the search with
self.backtrack(nums, 0, [], result): nothing is decided yet, so the index is 0 and the subset being built is empty. The helper is written next. - Return
result. The helper adds to this same list, so after the call it holds every subset. - Declare the helper
backtrack(self, nums, i, current, result)inside the class. Each call decides aboutnums[i]and calls itself for the next index. - Check for the end. When
i == len(nums), no number is left to decide about. - Save a copy and return.
current.copy()makes a new list with the same numbers. The copy is needed becausecurrentkeeps changing as the search goes on. - Skip. Call
self.backtrack(nums, i + 1, current, result)without touchingcurrent. This finds every subset withoutnums[i]. - Take. Add
nums[i]to the end ofcurrentwithappend. Numbers are added in index order, so each subset keeps the order ofnums. - Recurse again with the same arguments. This call finds every subset that contains
nums[i]. - Undo.
current.pop()removes the last number, which isnums[i], socurrentis what this call received.
The solution in Python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
result = []
self.backtrack(nums, 0, [], result)
return result
def backtrack(self, nums, i, current, result):
if i == len(nums):
result.append(current.copy())
return
self.backtrack(nums, i + 1, current, result)
current.append(nums[i])
self.backtrack(nums, i + 1, current, result)
current.pop()
Python details worth a closer look:
- Lists are shared, not copied. Passing
currentto a function passes the same list object, not a duplicate. That is what letsresultcollect the answers, and it is also why the copy matters. If the base case appendedcurrentitself,resultwould hold many references to one list, and since the search ends withcurrentempty, every entry would show it that way. Fornums = [1, 2, 3]that mistake gives[[], [], [], [], [], [], [], []]. - Forgetting the undo. Without
current.pop(), the taken numbers pile up across branches. Fornums = [1, 2]the result becomes[[], [2], [2, 1], [2, 1, 2]], which has the wrong subsets in the wrong order. - Copying a list.
current.copy(),list(current)andcurrent[:]all make a shallow copy. Here the elements are integers, so a shallow copy is all that's needed. - Methods and
self. The helper belongs to the class, so the calls are writtenself.backtrack(...), and the helper's definition listsselffirst. - Recursion depth. The recursion is one level per number, so at most 10 levels plus the first call. That is far below Python's default recursion limit of 1000.
Complexity
Time: O(n * 2^n). The search reaches 2^n finished subsets, and saving each one copies up to n numbers. The calls that don't reach the end are fewer than the ones that do, so they add only a constant factor.
Space: O(n) besides the answer. The extra memory is current, which holds at most n numbers, and one call on the stack per index. The answer itself, with 2^n subsets, is output and isn't counted.
Edge cases and common mistakes
- One number.
[0]gives[[], [0]]: two subsets, without the number and then with it. - Negative numbers and order.
[3, -1]gives[[], [-1], [3], [3, -1]]. The numbers inside a subset follownums, not their size, and the subsets follow the skip-before-take rule, not sorted order. - The empty subset. It is part of the answer, and it is always the first entry. It comes from skipping every number.
- Taking before skipping. Swapping the two recursive calls still finds all the subsets, but in a different order, so the lesson's tests would fail.
- Appending
currentwithout copying. This gives a list full of empty lists, as shown above. - Forgetting
returnin the base case. The code would then trynums[i]withi == len(nums)and raise anIndexError.
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]].nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]gives a list of 1024 subsets.
