Subsets Solution
Given an integer array of unique elements, return all possible subsets (the power set) without duplicates in any order.
Problem Statement
Given an integer array nums of unique elements, return all possible subsets (the power set). The solution set must not contain duplicate subsets and may be returned in any order.
Bitmask — O(n·2ⁿ)
For n elements there are 2ⁿ subsets. Represent each subset as an integer from 0 to 2ⁿ−1. Bit i being set means nums[i] is included. Iterate all integers and decode which bits are set.
Each number from 0 to 2ⁿ−1 is a unique binary pattern that perfectly encodes one possible include/exclude decision per element. No subset is generated twice and none is missed.
| Metric | Value |
|---|---|
| Time | O(n · 2ⁿ) — 2ⁿ masks × n bits each |
| Space | O(n · 2ⁿ) — output storage |
Backtracking — O(n·2ⁿ)
At each position in the array, make a binary decision: include or exclude the current element. Recurse into both branches, then undo the choice. Every leaf of the decision tree is a valid subset.
- State: current index
iand current subsetpath. - Base case: when
i == nums.length, add a copy ofpathto the result. - Include: push
nums[i], recurse withi+1. - Exclude: pop
nums[i], recurse withi+1(backtrack). - No need to sort — all elements are unique, order doesn't matter for correctness.
- The signal is "generate all possible combinations / subsets / partitions." You see the word "all" and the output is a list of lists.
- This exact template (index + path + choose/unchoose) solves LC 90 (Subsets II), LC 39 (Combination Sum), and LC 131 (Palindrome Partitioning) with minor modifications.
pathis a mutable list shared across all recursive calls.- If you add the reference directly to the result, every entry in the result will point to the same (eventually empty) list.
- Always add
new ArrayList<>(path)in Java orpath[:]in Python.
Visual Walkthrough
Trace for nums = [1, 2, 3]. The decision tree expands left (include) then right (exclude) at each level.
Code — Java & Python
Complexity Analysis
| Approach | Time | Space | Trade-off |
|---|---|---|---|
| Cascading (iterative) | O(n · 2ⁿ) | O(n · 2ⁿ) | Simple loop but hard to prune for constrained variants |
| Bitmask | O(n · 2ⁿ) | O(n · 2ⁿ) | Elegant but only works when n ≤ 32 |
| Backtracking ← optimal | O(n · 2ⁿ) | O(n) recursion + O(n · 2ⁿ) output | Generalizes to duplicates, constraints; O(n) call stack |
- Cascading works well for subsets but breaks when you add constraints like "no duplicates" (LC 90) or "must sum to target" (LC 39).
- Backtracking naturally prunes invalid paths before they're fully built, avoiding wasted work at each recursive step.
Edge Cases & Pitfalls
| Case | Input | Why It Matters |
|---|---|---|
| Single element | [5] | Two subsets: [] and [5]. Base case fires immediately on both branches. |
| Empty output row | any input | The empty set [] is always a valid subset — generated when all elements are excluded. |
| Negative numbers | [-1, 0, 1] | Algorithm is comparison-free; negatives work without special handling. |
| All zeros | [0, 0, 0] | Not possible per constraints (all elements unique), but illustrates why uniqueness matters. |
| Max input n=10 | 10 elements | 2¹⁰ = 1024 subsets. Well within memory. Stack depth = 10. |
| Shallow copy missing | any | Forgetting new ArrayList<>(path) causes all result entries to be the same empty list at the end. |
res.add(path) instead of res.add(new ArrayList<>(path)). Since path is reused across all recursive calls, every entry in res will reference the same list object. By the time recursion finishes, every "subset" will look like the final empty path. Always snapshot with a copy.