The problem
Given an array of unique integers, return every possible subset (the power set), in any order.
Example: nums = [1, 2, 3] → [[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]] — 2³ = 8 subsets.
Each element is either in or out of a subset, independently. That's 2ⁿ combinations, and the job is to enumerate them without duplicates or misses.
Approach 1 — Backtracking (include / exclude as you go)
Build one subset in a shared path array. At position start, loop over each candidate element, and for each: choose it (push), explore (recurse with the next start), unchoose (pop). Recording path at every node of the recursion — not just the leaves — captures all subsets.
function subsets(nums) {
const result = []
const path = []
function backtrack(start) {
result.push([...path]) // snapshot — path keeps mutating!
for (let i = start; i < nums.length; i++) {
path.push(nums[i]) // choose
backtrack(i + 1) // explore everything that starts this way
path.pop() // unchoose — restore state for the next branch
}
}
backtrack(0)
return result
}
Time: O(n · 2ⁿ) — 2ⁿ subsets, each copied in up to O(n). Space: O(n) recursion depth (beyond the output).
Two lines carry all the meaning:
result.push([...path])— the copy is mandatory; pushingpathitself would fill the result with references to one repeatedly-emptied array.path.pop()— the backtrack. Without it, choices leak from one branch into its siblings.
Approach 2 — Iterative doubling
Start with [[]]. For each number, extend every existing subset with it, keeping both versions.
function subsets(nums) {
let result = [[]]
for (const num of nums) {
result = result.concat(result.map((set) => [...set, num]))
}
return result
}
Same O(n · 2ⁿ). Elegant — and worth pairing with the observation that subset k corresponds to the bitmask of k: bit i set means take nums[i] (a third approach, useful when n ≤ 20).
Dry run
Backtracking on [1, 2, 3]. Each row is a call or an action inside one:
| Action | path | Recorded so far |
|---|---|---|
| enter backtrack(0), record [] | [] | 8 total: [] |
| choose 1, enter backtrack(1), record | [1] | …, [1] |
| choose 2, enter backtrack(2), record | [1, 2] | …, [1, 2] |
| choose 3, enter backtrack(3), record | [1, 2, 3] | …, [1, 2, 3] |
| pop 3, pop 2 — back in backtrack(1) | [1] | |
| choose 3, enter backtrack(3), record | [1, 3] | …, [1, 3] |
| pop 3, pop 1 — back in backtrack(0) | [] | |
| choose 2, enter backtrack(2), record | [2] | …, [2] |
| choose 3, enter backtrack(3), record | [2, 3] | …, [2, 3] |
| pop 3, pop 2, choose 3, record | [3] | …, [3] |
The push → recurse → pop rhythm visits each of the 8 subsets exactly once, in depth-first order.
Edge cases
- Empty input →
[[]]— the empty set is always a subset. - If the input can contain duplicates (Subsets II, LC 90): sort first, then inside the loop skip
nums[i] === nums[i - 1]wheni > start. Same skeleton, one guard line. - Output size is exponential by nature — no algorithm does better than O(2ⁿ) here, so don't chase it.