Permutations

Medium Backtracking

Problem

Given an array of distinct integers nums, return all possible permutations of nums in any order.

  • 1 ≤ nums.length ≤ 6
  • −10 ≤ nums[i] ≤ 10
  • All integers in nums are distinct.

Example

Input: nums = [1, 2, 3]
Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

The brute-force approach would attempt to generate all permutations by trying every possible ordering, but without pruning or systematic exploration, this can be inefficient and complex to implement. The backtracking approach incrementally builds permutations by choosing elements not yet used, exploring deeper only when the current partial permutation is valid. For example, starting with an empty list, the algorithm picks '1', then recursively picks '2', then '3', forming [1,2,3]. It then backtracks to replace '3' with '3' swapped with '2', forming [1,3,2], and so on, systematically exploring all valid permutations without repetition.

Approach

Straightforward Solution

A naive approach might try to generate all permutations by swapping elements in place or by generating all sequences and filtering duplicates, which is inefficient and error-prone. This approach can lead to redundant work or complex bookkeeping.

Core Observation

Generating permutations requires exploring all possible orderings of the input elements, which is factorial in size. The fundamental truth is that each position in the permutation can be filled by any unused element, and the problem breaks down into choosing an element and recursively permuting the remainder.

Path to Optimal

The key insight is to use backtracking with a partial solution that grows by adding unused elements. At each recursive call, the algorithm tries all elements not yet in the current permutation, appends one, and recurses. When the permutation length equals the input length, a complete permutation is found and added to the results. Backtracking removes the last element before trying the next candidate, restoring state for the next exploration. This approach systematically explores the solution space without duplicates and with clear state management.

Optimal Approach

Use a recursive backtracking function that maintains the current permutation path. For each call, iterate over all elements in nums, skip those already in the path, append a new element, recurse, then remove it to backtrack. This ensures all permutations are generated exactly once. The algorithm runs in O(n * n!) time, which is optimal for generating all permutations.

Time

O(n * n!)

There are n! permutations, and each permutation takes O(n) time to construct and copy into the results, leading to O(n * n!) total time.

Space

O(n)

The recursion stack and current path use O(n) space, where n is the length of nums. The output list uses O(n * n!) space but is not counted as auxiliary space.

Pattern Spotlight

Backtracking (State Restoration)

For problems requiring all permutations or combinations, use a 'choose, explore, un-choose' pattern where you add an element to the current path, recurse to explore deeper, then remove the element to restore state for the next choice.

Solution

1class Solution:
2 def permute(self, nums: List[int]) -> List[List[int]]:
3 def backtrack(curr):
4 if len(curr) == len(nums):
5 ans.append(curr[:])
6 return
7
8 for num in nums:
9 if num not in curr:
10 curr.append(num)
11 backtrack(curr)
12 curr.pop()
13
14 ans = []
15 backtrack([])
16 return ans

Step-by-Step Solution

1

Accumulate Complete Permutations Upon Reaching Full Length

4if len(curr) == len(nums):
5 ans.append(curr[:])
6 return

Objective

To detect when a complete permutation has been formed and add a copy to the results list.

Key Insight

The base case of the recursion occurs when the current path length equals the input length, indicating a full permutation. Appending a copy of the current path ensures that subsequent modifications to the path do not affect the stored result. This step is critical to correctly capture each unique permutation.

Interview Quick-Check

Core Logic

Recognize the base case by comparing current path length to input length and append a copy to results to avoid mutation issues.

State & Boundaries

The base case prevents further recursion and signals a valid solution has been found.

2

Explore Candidates by Adding Unused Elements and Backtracking

8for num in nums:
9 if num not in curr:
10 curr.append(num)
11 backtrack(curr)
12 curr.pop()

Objective

To iterate over all input elements, add unused ones to the current path, recurse, and then remove them to restore state.

Key Insight

At each recursion level, the algorithm tries every element not already in the current path, ensuring no duplicates in the permutation. Adding an element extends the partial solution, and recursion explores deeper permutations. The pop operation after recursion is the 'un-choose' step that restores the path to its previous state, enabling exploration of alternative branches. This pattern guarantees exhaustive and non-redundant exploration of all permutations.

Interview Quick-Check

Core Logic

Use a for loop to try all elements not in the current path, append one, recurse, then pop to backtrack.

Common Pitfalls & Bugs

Failing to pop after recursion leads to incorrect paths and duplicates.

State & Boundaries

The 'if num not in curr' condition ensures elements are not reused in the same permutation.

3

Initialize Result Container and Launch Backtracking

14ans = []
15backtrack([])

Objective

To prepare the results list and start the recursive backtracking with an empty path.

Key Insight

Initializing an empty list to store permutations is essential for collecting results. Starting the recursion with an empty path represents the initial state before any elements are chosen. This setup cleanly separates initialization from recursive exploration.

Interview Quick-Check

Core Logic

Initialize an empty list to store permutations and start recursion with an empty current path.

4

Return the Complete List of Permutations

16return ans

Objective

To return the accumulated list of all generated permutations after backtracking completes.

Key Insight

Returning the results list after the recursive calls ensures the caller receives all permutations generated by the backtracking process. This final step completes the algorithm.

Interview Quick-Check

Core Logic

Return the results list containing all permutations after recursion finishes.

Line Analysis

This solution has 3 Critical lines interviewers watch for.

Line 12 Critical
curr.pop()

Remove the last added number to restore the path for the next iteration.

This 'un-choose' step is critical for backtracking, restoring state so other permutations can be explored without interference.

Line 4 Critical
if len(curr) == len(nums):

Check if the current permutation has reached the length of the input array.

This base case identifies when a complete permutation has been formed, signaling that the current path is a valid solution to be recorded.

Line 5 Critical
ans.append(curr[:])

Append a copy of the current permutation to the results list.

Appending a copy prevents future modifications to the current path from altering stored permutations, preserving correctness.

Line 9 Core
if num not in curr:

Check if the current number is already in the current permutation path.

This condition prevents reuse of the same element in a single permutation, maintaining the uniqueness constraint.

Line 11 Core
backtrack(curr)

Recursively explore permutations with the updated path.

This recursive call advances the search, building permutations by adding one element at a time.

Line 6 Core
return

Return to stop further recursion once a complete permutation is recorded.

This return prevents unnecessary exploration beyond a complete permutation, ensuring the recursion unwinds correctly.

Line 10 Core
curr.append(num)

Add the current number to the permutation path.

Appending the number extends the partial solution, allowing deeper exploration of permutations including this element.

Line 8 Standard
for num in nums:

Iterate over each number in the input array to consider it for the current position.

This loop explores all possible candidates for the next position in the permutation, enabling exhaustive search.

Line 15 Standard
backtrack([])

Start the backtracking process with an empty current path.

This initial call triggers the recursive exploration of all permutations from an empty state.

Line 14 Standard
ans = []

Initialize the list to store all permutations.

This list accumulates all valid permutations found during recursion, serving as the final output container.

Line 16 Standard
return ans

Return the list of all generated permutations.

This return statement provides the final answer after all recursive exploration completes.

Test Your Understanding

Why is it necessary to remove the last element from the current path after each recursive call?

Removing the last element restores the state of the current path so that the algorithm can explore other permutations without carrying over choices from previous branches, ensuring correct and complete exploration of all permutations.

Related Problems

Backtracking pattern

Don't just read it. Drill it.

Reconstruct Permutations from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Permutations drill

or drill a free problem