Subsets

Subsets is the question that teaches you backtracking. So if you can do this one well, a whole family of interview problems opens up. The trick is not the answer. The trick is the pattern you use to build the answer. Learn that pattern here and you reuse it everywhere.

🎯 The Problem

You get an array of distinct numbers. You must return every possible subset.

  • A subset is any selection of the numbers.
  • The empty selection [] counts.
  • The full array counts too.
  • Every number is either taken or skipped.
  • The full collection of all subsets is called the power set.
  • The order of the subsets does not matter.
  • No subset may be missing and none may repeat.
Input: nums = [1, 2, 3]
Output: [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
Explanation: every possible selection of the numbers, including the empty one.

Here is the problem drawn as a choice for each number. For each one you either take it or you skip it.

start: []

take 1: [1]

skip 1: []

take 2

skip 2

take 2

skip 2

...take or skip 3

...take or skip 3

...take or skip 3

...take or skip 3

🐒 Approach 1: Iterative Build-Up (Brute Force)

The idea in one line: start with the empty subset, then double the list for each new number.

The idea:

  • Begin with a list that holds only [].
  • Look at each number one at a time.
  • For that number, copy every subset you already have.

How it works:

  • Add the new number to each copy.
  • Keep both the old subsets and the new ones.
  • Start []. Add 1: you get [] and [1]. Add 2: you also get [2] and [1,2].
  • Each new number doubles the count.

Why it is weak:

  • It needs no recursion, so it is easy to picture.
  • But it copies the whole growing list every step.
  • Interviewers usually want the recursive pattern, since it solves the harder problems too.

Here is the iterative build-up code:

subsets_iterative.py
def subsets(nums):
result = [[]]
for num in nums:
result += [path + [num] for path in result]
return result

⚑ Approach 2: Backtracking (Best)

The idea in one line: build the subset step by step, recurse to go deeper, then undo your last choice.

The idea:

  • Backtracking means choose, explore, then un-choose.
  • Keep a current path, which is the subset you are building right now.
  • Every node you visit is itself a valid subset.

How it works:

  • Record a copy of the path at the very start of each call.
  • Loop over the numbers you may still add.
  • Add one to the path. This is the choose step.
  • Recurse to pick the next. This is the explore step.
  • Remove that number when the recursion returns. This is the un-choose step.

Why it is fast:

  • Each subset is built once, with no big list copies along the way.
  • The un-choose keeps the path matching exactly the choices on the way down.
  • Recording at the top captures the empty subset, the singles, the pairs, and the full set.

Why the un-choose matters:

  • Without it, the path keeps old numbers from a branch you already finished.
  • With it, the path is always clean for the next choice.
  • That is what makes backtracking correct.

This walk through the choices, with the un-choose on the way back up, draws the recursion tree. The picture below shows it.

path=[] record []

add 1, path=[1] record [1]

add 2, path=[1,2] record [1,2]

add 3, path=[1,2,3] record

un-choose 3

add 3, path=[1,3] record [1,3]

add 2, path=[2] record [2]

add 3, path=[2,3] record [2,3]

add 3, path=[3] record [3]

Steps to Solve

  1. Create a result list to hold every subset.
  2. Start a recursive helper with an empty path and a start index of 0.
  3. At the top of each call, record a copy of the current path as one subset.
  4. Loop over the numbers from the start index to the end.
  5. Add the current number to the path. This is the choose step.
  6. Recurse with the start index moved one past the current number.
  7. Remove the number you just added. This is the un-choose step.
  8. When every branch finishes, return the result.

This Python version uses a list as the path and appends a copy into the result at each call.

subsets.py
def subsets(nums):
result = []
path = []
def backtrack(start):
result.append(path[:]) # record current subset (a copy)
for i in range(start, len(nums)):
path.append(nums[i]) # choose
backtrack(i + 1) # explore
path.pop() # un-choose
backtrack(0)
return result
print(subsets([1, 2, 3]))

The output of the above code will be:

[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

Let us walk through the Python version line by line, because the recursion is where people get lost.

result = [] and path = [] set up two lists. The result holds every finished subset. The path holds the subset we are building right now.

result.append(path[:]) runs at the very top of each call. The path[:] makes a copy of the current path. We must copy it, because the path keeps changing. If we stored the path itself, every entry in the result would later point to the same emptied list. The copy freezes it at this moment.

for i in range(start, len(nums)) loops over the numbers we are still allowed to pick. The start index stops us from reusing earlier numbers, which is what keeps subsets from repeating.

path.append(nums[i]) is the choose step. We add this number to the path.

backtrack(i + 1) is the explore step. We recurse to pick from the numbers after this one.

path.pop() is the un-choose step. After the recursion returns, we remove the number we just added. This resets the path so the next loop turn starts clean. Miss this line and the path carries garbage from finished branches.

⏱️ Time and Space Complexity

There are 2 to the power n subsets, because each number is either in or out. So the count alone is exponential. Backtracking visits each subset once and copies it, which costs up to n per copy. The iterative way has the same shape. Both land at O(n times 2^n) time. The space, not counting the output, is O(n) for the recursion depth and the path.

Approach Time Complexity Space Complexity
Iterative build-up O(n Β· 2^n) O(n) extra
Backtracking O(n Β· 2^n) O(n) extra

Tip

The choose, explore, un-choose shape is the same for almost every backtracking problem. Learn it once on Subsets. Then permutations, combinations, and word search all feel familiar.

🧩 Key Takeaways

  • βœ… A subset is any selection of the numbers, including the empty one and the full one.
  • βœ… Backtracking means choose, explore, then un-choose your last move.
  • βœ… Record a copy of the path at the top of each call, so every node becomes a subset.
  • βœ… The start index stops you reusing earlier numbers, which avoids repeats.
  • βœ… There are 2^n subsets, so the time is O(n Β· 2^n) no matter the method.

Check Your Knowledge

4 questions Show quiz Hide quiz

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does a subset include in the Subsets problem?

    Why: The power set includes the empty subset, every single number, every combination, and the full set.

  2. 2

    What are the three steps of backtracking?

    Why: Backtracking builds a candidate (choose), recurses (explore), then undoes the last choice (un-choose).

  3. 3

    Why do we copy the path before adding it to the result?

    Why: The path is mutated throughout. Without a copy, every stored subset would point to the same later-emptied list.

  4. 4

    How many subsets does an array of n distinct numbers have?

    Why: Each number is either in or out, so there are 2^n total subsets.

πŸš€ What’s Next?