Facebook Pixel

78. Subsets

MediumBit ManipulationArrayBacktracking
Leetcode Link

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
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. First explore the path where we don't include the current element (just move to the next index)
  2. 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:

  1. Exclude the current element: We directly call dfs(i + 1) without adding nums[i] to our subset. This explores all subsets that don't contain nums[i].

  2. Include the current element: We add nums[i] to our current subset t using t.append(nums[i]), then call dfs(i + 1) to explore all subsets that contain nums[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 subsets
  • t: 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 Evaluator

Example 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:

  1. dfs(0) - Deciding on element 1:

    • First, exclude 1: call dfs(1) with t = []
  2. dfs(1) - Deciding on element 2 (with 1 excluded):

    • First, exclude 2: call dfs(2) with t = []
  3. dfs(2) - Deciding on element 3 (with 1,2 excluded):

    • First, exclude 3: call dfs(3) with t = []
  4. dfs(3) - Base case reached: Add [] to answer ✓

    • Backtrack to dfs(2)
    • Now include 3: t = [3], call dfs(3)
  5. dfs(3) - Base case: Add [3] to answer ✓

    • Pop 3, backtrack to dfs(1)
    • Now include 2: t = [2], call dfs(2)
  6. dfs(2) - Deciding on element 3 (with 2 included):

    • First, exclude 3: call dfs(3) with t = [2]
  7. dfs(3) - Base case: Add [2] to answer ✓

    • Backtrack to dfs(2)
    • Now include 3: t = [2, 3], call dfs(3)
  8. dfs(3) - Base case: Add [2, 3] to answer ✓

    • Pop 3, then pop 2, backtrack to dfs(0)
    • Now include 1: t = [1], call dfs(1)
  9. dfs(1) - Deciding on element 2 (with 1 included):

    • First, exclude 2: call dfs(2) with t = [1]
  10. dfs(2) - Deciding on element 3 (with 1 included, 2 excluded):

    • First, exclude 3: call dfs(3) with t = [1]
  11. dfs(3) - Base case: Add [1] to answer ✓

    • Backtrack to dfs(2)
    • Now include 3: t = [1, 3], call dfs(3)
  12. dfs(3) - Base case: Add [1, 3] to answer ✓

    • Pop 3, backtrack to dfs(1)
    • Now include 2: t = [1, 2], call dfs(2)
  13. dfs(2) - Deciding on element 3 (with 1,2 included):

    • First, exclude 3: call dfs(3) with t = [1, 2]
  14. dfs(3) - Base case: Add [1, 2] to answer ✓

    • Backtrack to dfs(2)
    • Now include 3: t = [1, 2, 3], call dfs(3)
  15. 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 in nums)
  • The temporary list t which stores the current subset being constructed, having at most n 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More