78. Subsets
Problem Description
You are given an integer array nums
that contains unique elements (no duplicates). Your task is to generate and return all possible subsets of this array, also known as the power set.
A subset is a collection that can be formed by including or excluding each element from the original array. The empty subset (containing no elements) and the full array itself are both valid subsets.
Important requirements:
- The solution must include all possible subsets - from the empty set to the full array
- There should be no duplicate subsets in your result
- The subsets can be returned in any order
For example, if nums = [1, 2, 3]
, the power set would be:
[]
(empty subset)[1]
[2]
[3]
[1, 2]
[1, 3]
[2, 3]
[1, 2, 3]
(full array)
The total number of subsets for an array of n
elements is always 2^n
, since each element has two choices: either be included in the subset or not.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem does not involve nodes and edges or graph traversal. We're working with an array of integers and need to generate subsets.
Need to solve for kth smallest/largest?
- No: We're not looking for a specific kth element. We need to generate all possible subsets.
Involves Linked Lists?
- No: The problem works with an array, not linked lists.
Does the problem have small constraints?
- Yes: Generating all subsets means we need to explore
2^n
possibilities. This exponential growth is only feasible with small constraints (typically n ≤ 20). The problem asks us to generate ALL possible combinations, which inherently suggests manageable input sizes.
Brute force / Backtracking?
- Yes: Since we need to explore all possible combinations of including or excluding each element, backtracking is the perfect approach. For each element, we make a choice (include it or not), explore further, and then backtrack to explore other possibilities.
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This makes sense because:
- We need to generate all possible combinations (exhaustive search)
- Each element has a binary choice (include/exclude)
- We can build solutions incrementally and backtrack when needed
- The problem space forms a decision tree where we explore all paths
Intuition
The key insight for generating all subsets is recognizing that for each element in the array, we have exactly two choices: either include it in the current subset or exclude it. This binary decision-making process naturally forms a decision tree.
Imagine you're packing for a trip and have n
items. For each item, you decide: "take it" or "leave it". After making decisions for all items, you have one possible packing configuration (subset). To find all possible configurations, you'd need to explore every combination of decisions.
This is exactly what the backtracking solution does. Starting from index 0, we:
- First explore the path where we don't include the current element (just move to the next index)
- Then explore the path where we do include the current element (add it to our current subset, then move to the next index)
The beauty of this approach is that it naturally builds a complete decision tree. Each leaf node in this tree represents a complete subset (after we've made decisions for all elements). Since we have n
elements and 2 choices for each, we get 2^n
total subsets.
The backtracking happens when we've explored one branch completely - we remove the element we just added (the t.pop()
operation) to restore our subset to its previous state, allowing us to explore other branches from that point.
Think of it like exploring a maze where at each intersection you can go left (exclude) or right (include). You systematically explore all paths: first go left at every intersection until you reach the end, record that path, then backtrack and try going right at the last intersection, and so on, until you've mapped every possible route through the maze.
Learn more about Backtracking patterns.
Solution Approach
The implementation uses a DFS (Depth-First Search) with backtracking to generate all subsets. Let's walk through the key components:
Core Algorithm Structure:
We define a recursive function dfs(i)
that processes elements starting from index i
. This function represents our decision point for the element at position i
in the array.
Base Case:
When i == len(nums)
, we've made decisions for all elements. At this point, we add a copy of the current subset t
to our answer array ans
. We use t[:]
to create a copy because t
will continue to be modified as we backtrack.
Recursive Cases:
For each element at index i
, we explore two branches:
-
Exclude the current element: We directly call
dfs(i + 1)
without addingnums[i]
to our subset. This explores all subsets that don't containnums[i]
. -
Include the current element: We add
nums[i]
to our current subsett
usingt.append(nums[i])
, then calldfs(i + 1)
to explore all subsets that containnums[i]
.
Backtracking Step:
After exploring the "include" branch, we need to restore our subset to its previous state. We do this with t.pop()
, which removes the element we just added. This is crucial - it allows us to backtrack to the previous decision point and explore other possibilities.
Data Structures Used:
ans
: A list to store all generated subsetst
: A temporary list that represents the current subset being built- The recursion stack implicitly maintains our position in the decision tree
Execution Flow:
Starting with dfs(0)
, the algorithm systematically explores all 2^n
paths through the decision tree. Each path from root to leaf represents one unique subset, ensuring we generate all possible combinations without duplicates.
The time complexity is O(n × 2^n)
since we generate 2^n
subsets and each subset operation (copying) takes O(n)
time. The space complexity is O(n)
for the recursion stack depth.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with nums = [1, 2, 3]
to see how it generates all 8 subsets.
We start with an empty temporary subset t = []
and call dfs(0)
.
Step-by-step execution:
-
dfs(0) - Deciding on element 1:
- First, exclude 1: call
dfs(1)
witht = []
- First, exclude 1: call
-
dfs(1) - Deciding on element 2 (with 1 excluded):
- First, exclude 2: call
dfs(2)
witht = []
- First, exclude 2: call
-
dfs(2) - Deciding on element 3 (with 1,2 excluded):
- First, exclude 3: call
dfs(3)
witht = []
- First, exclude 3: call
-
dfs(3) - Base case reached: Add
[]
to answer ✓- Backtrack to dfs(2)
- Now include 3:
t = [3]
, calldfs(3)
-
dfs(3) - Base case: Add
[3]
to answer ✓- Pop 3, backtrack to dfs(1)
- Now include 2:
t = [2]
, calldfs(2)
-
dfs(2) - Deciding on element 3 (with 2 included):
- First, exclude 3: call
dfs(3)
witht = [2]
- First, exclude 3: call
-
dfs(3) - Base case: Add
[2]
to answer ✓- Backtrack to dfs(2)
- Now include 3:
t = [2, 3]
, calldfs(3)
-
dfs(3) - Base case: Add
[2, 3]
to answer ✓- Pop 3, then pop 2, backtrack to dfs(0)
- Now include 1:
t = [1]
, calldfs(1)
-
dfs(1) - Deciding on element 2 (with 1 included):
- First, exclude 2: call
dfs(2)
witht = [1]
- First, exclude 2: call
-
dfs(2) - Deciding on element 3 (with 1 included, 2 excluded):
- First, exclude 3: call
dfs(3)
witht = [1]
- First, exclude 3: call
-
dfs(3) - Base case: Add
[1]
to answer ✓- Backtrack to dfs(2)
- Now include 3:
t = [1, 3]
, calldfs(3)
-
dfs(3) - Base case: Add
[1, 3]
to answer ✓- Pop 3, backtrack to dfs(1)
- Now include 2:
t = [1, 2]
, calldfs(2)
-
dfs(2) - Deciding on element 3 (with 1,2 included):
- First, exclude 3: call
dfs(3)
witht = [1, 2]
- First, exclude 3: call
-
dfs(3) - Base case: Add
[1, 2]
to answer ✓- Backtrack to dfs(2)
- Now include 3:
t = [1, 2, 3]
, calldfs(3)
-
dfs(3) - Base case: Add
[1, 2, 3]
to answer ✓
Final Result: [[], [3], [2], [2,3], [1], [1,3], [1,2], [1,2,3]]
The decision tree visualization:
Start (i=0, t=[]) / \ Exclude 1 Include 1 (i=1, t=[]) (i=1, t=[1]) / \ / \ Exclude 2 Include 2 Exclude 2 Include 2 (i=2,t=[]) (i=2,t=[2]) (i=2,t=[1]) (i=2,t=[1,2]) / \ / \ / \ / \ [] [3] [2] [2,3] [1] [1,3] [1,2] [1,2,3]
Each path from root to leaf represents one subset. The backtracking ensures we explore all branches systematically, generating all 2³ = 8 possible subsets.
Solution Implementation
1class Solution:
2 def subsets(self, nums: List[int]) -> List[List[int]]:
3 """
4 Generate all possible subsets of the given array.
5
6 Args:
7 nums: List of integers
8
9 Returns:
10 List of all possible subsets
11 """
12
13 def backtrack(index: int) -> None:
14 """
15 Recursive helper function to generate subsets using backtracking.
16
17 Args:
18 index: Current index in the nums array
19 """
20 # Base case: reached the end of the array
21 if index == len(nums):
22 # Add a copy of current subset to the result
23 result.append(current_subset[:])
24 return
25
26 # Choice 1: Exclude the current element
27 backtrack(index + 1)
28
29 # Choice 2: Include the current element
30 current_subset.append(nums[index])
31 backtrack(index + 1)
32
33 # Backtrack: remove the element we just added
34 current_subset.pop()
35
36 # Initialize result list to store all subsets
37 result = []
38
39 # Initialize temporary list to build current subset
40 current_subset = []
41
42 # Start the backtracking process from index 0
43 backtrack(0)
44
45 return result
46
1class Solution {
2 // List to store all subsets
3 private List<List<Integer>> result = new ArrayList<>();
4 // Temporary list to build current subset
5 private List<Integer> currentSubset = new ArrayList<>();
6 // Input array reference
7 private int[] nums;
8
9 /**
10 * Generate all possible subsets of the given array
11 * @param nums Input array of integers
12 * @return List containing all possible subsets
13 */
14 public List<List<Integer>> subsets(int[] nums) {
15 this.nums = nums;
16 backtrack(0);
17 return result;
18 }
19
20 /**
21 * Recursive backtracking function to generate subsets
22 * @param index Current index in the nums array
23 */
24 private void backtrack(int index) {
25 // Base case: reached end of array, add current subset to result
26 if (index == nums.length) {
27 result.add(new ArrayList<>(currentSubset));
28 return;
29 }
30
31 // Choice 1: Exclude current element from subset
32 backtrack(index + 1);
33
34 // Choice 2: Include current element in subset
35 currentSubset.add(nums[index]);
36 backtrack(index + 1);
37
38 // Backtrack: remove the element we just added
39 currentSubset.remove(currentSubset.size() - 1);
40 }
41}
42
1class Solution {
2public:
3 vector<vector<int>> subsets(vector<int>& nums) {
4 vector<vector<int>> result; // Stores all subsets
5 vector<int> currentSubset; // Temporary array to build each subset
6
7 // Recursive function to generate all subsets using backtracking
8 function<void(int)> generateSubsets = [&](int index) -> void {
9 // Base case: reached the end of the input array
10 if (index == nums.size()) {
11 result.push_back(currentSubset); // Add current subset to result
12 return;
13 }
14
15 // Choice 1: Exclude current element from subset
16 generateSubsets(index + 1);
17
18 // Choice 2: Include current element in subset
19 currentSubset.push_back(nums[index]);
20 generateSubsets(index + 1);
21
22 // Backtrack: remove the element we just added
23 currentSubset.pop_back();
24 };
25
26 // Start the recursive generation from index 0
27 generateSubsets(0);
28
29 return result;
30 }
31};
32
1/**
2 * Generates all possible subsets of the given array
3 * @param nums - Input array of numbers
4 * @returns Array containing all possible subsets
5 */
6function subsets(nums: number[]): number[][] {
7 // Store all generated subsets
8 const result: number[][] = [];
9 // Temporary array to build current subset
10 const currentSubset: number[] = [];
11
12 /**
13 * Depth-first search to generate subsets
14 * @param index - Current index in the nums array
15 */
16 const generateSubsets = (index: number): void => {
17 // Base case: reached the end of array
18 if (index === nums.length) {
19 // Add a copy of current subset to results
20 result.push([...currentSubset]);
21 return;
22 }
23
24 // Choice 1: Exclude current element from subset
25 generateSubsets(index + 1);
26
27 // Choice 2: Include current element in subset
28 currentSubset.push(nums[index]);
29 generateSubsets(index + 1);
30
31 // Backtrack: remove the element we just added
32 currentSubset.pop();
33 };
34
35 // Start DFS from index 0
36 generateSubsets(0);
37
38 return result;
39}
40
Time and Space Complexity
Time Complexity: O(n × 2^n)
The algorithm explores a binary decision tree where at each level i
, we decide whether to include nums[i]
in the current subset or not. This creates 2^n
total subsets (where n
is the length of the input array). For each subset, when we reach the base case (i == len(nums)
), we perform a copy operation t[:]
which takes O(n)
time to create a copy of the current subset. Therefore, the total time complexity is O(n × 2^n)
.
Space Complexity: O(n)
The space complexity consists of:
- The recursion call stack depth, which can go up to
n
levels deep (one for each element innums
) - The temporary list
t
which stores the current subset being constructed, having at mostn
elements - The output list
ans
is not counted as part of the auxiliary space complexity since it's required for the output
The dominant factor is the recursion depth and the temporary list, both of which are O(n)
. Note that while the total output requires O(n × 2^n)
space to store all subsets, the auxiliary space complexity (excluding output) is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Create a Copy of the Current Subset
The Pitfall: One of the most common mistakes is adding the current subset directly to the result without creating a copy:
# WRONG - This will cause all subsets to reference the same list
if index == len(nums):
result.append(current_subset) # ❌ Direct reference
return
Why This Fails:
Since current_subset
is a mutable list that gets modified throughout the recursion, all entries in result
will end up pointing to the same list object. By the end of execution, result
will contain multiple references to an empty list.
The Solution: Always create a copy of the current subset before adding it to the result:
# CORRECT - Creates a new list with the current elements
if index == len(nums):
result.append(current_subset[:]) # ✅ Creates a copy
# Alternative ways to copy:
# result.append(list(current_subset))
# result.append(current_subset.copy())
return
2. Forgetting the Backtracking Step
The Pitfall: Another critical error is forgetting to remove the element after exploring the "include" branch:
# WRONG - Missing the pop() operation backtrack(index + 1) current_subset.append(nums[index]) backtrack(index + 1) # ❌ Forgot to pop the element!
Why This Fails:
Without removing the element, the current_subset
will keep accumulating elements from different branches of the decision tree. This corrupts the subset generation process and produces incorrect results.
The Solution: Always restore the state after exploring a branch:
# CORRECT - Properly backtrack after exploring backtrack(index + 1) current_subset.append(nums[index]) backtrack(index + 1) current_subset.pop() # ✅ Essential backtracking step
3. Incorrect Order of Operations
The Pitfall: Placing the recursive calls in the wrong order relative to the append/pop operations:
# WRONG - Append before both recursive calls current_subset.append(nums[index]) backtrack(index + 1) # This explores with element included backtrack(index + 1) # This also has element included! current_subset.pop()
Why This Fails:
Both recursive calls will explore subsets that include nums[index]
, missing all subsets that exclude this element.
The Solution: Follow the correct pattern: exclude first, then include-explore-backtrack:
# CORRECT - Proper order of operations backtrack(index + 1) # Explore without current element current_subset.append(nums[index]) backtrack(index + 1) # Explore with current element current_subset.pop() # Backtrack
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!