Problem
Given an array of unique integers, return every possible subset of those integers, including the empty set. Each element either appears in a subset or it doesn't โ there's no repetition of elements within a subset, and [1,2] and [2,1] count as the same subset.
- Input:
nums = [1, 2, 3] - Output:
[[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]] - Explanation: All 8 (2ยณ) subsets of a 3-element array, including the empty subset.
Intuition
Every element faces exactly one binary choice: be included or be excluded. That gives exactly 2^n subsets for n elements. We can build them iteratively โ each new element doubles the collection by appending it to every existing subset โ or recursively with a backtracking DFS that records the current partial subset at every node in the decision tree.
Approach 1 โ Iterative Cascading
Start with just the empty subset. For each element, generate new subsets by adding that element to every subset already collected, then extend the collection with those new subsets.
- Initialize
subsetswith one entry: the empty list. - For each
numinnums:- Snapshot the current size of
subsetsbefore extending. - For every existing subset (up to the snapshot), create a copy with
numappended. - Add those new subsets to
subsets.
- Snapshot the current size of
- Return
subsets.
1def subsets(nums):
2 result = [[]]
3 for num in nums:
4 # each existing subset spawns a copy that includes num
5 result += [subset + [num] for subset in result]
6 return resultTime: O(n ร 2^n) โ each of n elements is added to all 2^(iโ1) subsets collected so far.
Space: O(n ร 2^n) โ the output itself; no additional stack or auxiliary structure.
Approach 2 โ Backtracking
Use DFS: at each call, record the current path as a valid subset (every partial state is valid for this problem), then try extending it with each remaining element one at a time, backtracking after each choice.
- Start DFS with an empty
pathandstart_index = 0. - At the top of each call, append a copy of
pathtoresult. - Loop
ifromstart_indexto the end ofnums:- Add
nums[i]topath. - Recurse with
start_index = i + 1so elements aren't reused. - Pop
nums[i]frompathto undo the choice.
- Add
- Return
result.
1def subsets(nums):
2 result = []
3
4 def backtrack(start_index, path):
5 result.append(path[:]) # copy โ not a reference to the mutable list
6 for i in range(start_index, len(nums)):
7 path.append(nums[i])
8 backtrack(i + 1, path)
9 path.pop() # undo before trying the next element
10
11 backtrack(0, [])
12 return resultTime: O(n ร 2^n) โ 2^n subsets, each of average length n/2.
Space: O(n) for the recursion stack and the path buffer, plus O(n ร 2^n) for the output.
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| Iterative Cascading | O(n ร 2^n) | O(n ร 2^n) | Simple, no recursion โ good when a direct loop feels cleaner |
| Backtracking | O(n ร 2^n) | O(n) + output | Natural template for Subsets II, combinations with pruning, or constrained enumeration |
Common Mistakes
- Storing a reference instead of a copy โ
result.append(path)in Python records the same list object, so every "stored" subset silently changes aspathmutates. Always usepath[:]in Python ornew ArrayList<>(path)in Java. - Iterating over
resultwhile modifying it โ in the iterative approach, snapshotcurrentSize = len(result)before the inner loop; without it you'll process newly added subsets as inputs and generate duplicates. - Forgetting the empty subset โ the empty set is always a valid subset. In iterative, start with
[[]]. In backtracking, addingpath[:]before the loop (not after) ensures the empty path is captured at the root. - Passing
iinstead ofi + 1to the recursive call โbacktrack(i, path)allows the same index to be revisited, producing repeated elements in subsets rather than distinct subsets. - Assuming the output must be sorted โ the problem does not require a specific order. If a test harness does a direct equality check, sort both the subsets themselves and the outer list before comparing.
Related Problems
subsets-iiโ same problem but the input contains duplicates, requiring a sort + skip-duplicate step inside the backtracking looppermutationsโ also enumerates all combinations via backtracking, but order matters so elements can be revisited from a "used" set instead of a start indexcombination-sum-iiโ backtracking over candidates with a target sum and duplicate-skipping, built on the same DFS skeletonpalindrome-partitioningโ enumerate all ways to partition a string, using the same "add current result, then try each extension" backtracking patternletter-combinations-of-a-phone-numberโ enumerate all combinations from a digit mapping; the same backtracking template applied to a fixed branching factor