Permutations

Permutations is the classic ordering problem. You take a set of numbers and list every possible order of them. Here order matters, which makes it different from Subsets. The interviewer wants to see if you can track which numbers are still free to use, and undo cleanly. That tracking is the core skill.

🎯 The Problem

You get an array of distinct numbers. You return every possible ordering of them.

The rules:

  • An ordering that uses all the numbers in some sequence is a permutation.
  • Order matters. So [1,2,3] and [2,1,3] are different answers.
  • Each permutation uses every number exactly once.
  • The order of the permutations in the answer does not matter.
Input: nums = [1, 2, 3]
Output: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
Explanation: every possible ordering of all the numbers.

Here is the problem as a tree where each level fixes one position, and each branch picks a number not yet used.

start []

pick 1

pick 2

pick 3

pick 2 -> [1,2,3]

pick 3 -> [1,3,2]

pick 1 -> [2,1,3]

pick 3 -> [2,3,1]

πŸ” Approach 1: Swap In Place (Alternative)

The idea in one line: fix each position by swapping every later number into it, then swap back.

How it works:

  • At position i, swap each index from i to the end into position i.
  • Recurse on position i + 1.
  • Swap the numbers back to restore the array.
  • At the last position, the array itself is one permutation. Record it.

Why it works:

  • It uses no extra used array.
  • The array doubles as both the path and the bookkeeping.

Why it is trickier:

  • The swap-back is easy to forget, which corrupts later branches.
  • The output order is harder to predict than the used-array version.

Here is the in-place swap code for that idea:

permutations_swap_in_place.py
def permute(nums):
result = []
def dfs(index):
if index == len(nums):
result.append(nums[:])
return
for i in range(index, len(nums)):
nums[index], nums[i] = nums[i], nums[index]
dfs(index + 1)
nums[index], nums[i] = nums[i], nums[index]
dfs(0)
return result

⚑ Approach 2: Backtracking With a Used Array (Best)

The idea in one line: build the ordering step by step, track which numbers are taken with a flag list, and undo cleanly.

How backtracking works here:

  • Build a candidate step by step. Recurse deeper. Then undo the last choice.
  • The candidate is the ordering built so far.

Why a used array:

  • Unlike Subsets, we do not move a start index forward.
  • A permutation may pick a number that sits earlier in the array.
  • A used array is a list of true and false flags, one per number.
  • Each flag says whether that number is already in the path.

How it works:

  • At each level loop over every number.
  • Already used, skip it.
  • Otherwise mark it used, add it to the path, recurse to fill the next position.
  • On return, remove the number and mark it unused. That un-choose step frees it for other branches.

When a path finishes:

  • The path holds all the numbers. It is a complete permutation.
  • Record a copy and stop that branch.
  • The recursion depth equals the count of numbers.

The picture below dry-runs [1, 2, 3]. Marking and un-marking the used flags lets each number appear in every position.

path=[] used=FFF

use 1, path=[1] used=TFF

use 2, path=[1,2] used=TTF

use 3, path=[1,2,3] RECORD

un-choose 3, then un-choose 2

use 3, path=[1,3] used=TFT

use 2, path=[1,3,2] RECORD

Steps to Solve

  1. Create a result list, a path for the current ordering, and a used array of all false.
  2. Start a recursive helper.
  3. If the path length equals the number count, record a copy of the path and return.
  4. Loop over every index in the array.
  5. If that index is already used, skip it.
  6. Mark the index used and add its number to the path. This is the choose step.
  7. Recurse to fill the next position.
  8. Remove the number and mark the index unused again. This is the un-choose step.

This Python version keeps a used list of booleans and records the path when it holds every number.

permutations.py
def permute(nums):
result = []
path = []
used = [False] * len(nums)
def backtrack():
if len(path) == len(nums): # path holds every number
result.append(path[:])
return
for i in range(len(nums)):
if used[i]: # this number is already in the path
continue
used[i] = True # choose
path.append(nums[i])
backtrack() # explore
path.pop() # un-choose
used[i] = False
backtrack()
return result
print(permute([1, 2, 3]))

The output of the above code will be:

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

Let us walk through the Python version line by line, because the used array is what makes permutations different.

used = [False] * len(nums) builds a flag list, one per number, all false at the start. Each flag says whether that number is already in the current path.

if len(path) == len(nums) checks for a complete ordering. When the path holds every number, it is a full permutation, so we record a copy with path[:] and return. The copy is needed because the path keeps changing.

for i in range(len(nums)) loops over every index. Notice we start at 0 every time, not from a moving start index. That is the key difference from Subsets, because a permutation may use a number that sits earlier in the array.

if used[i]: continue skips a number that is already in the path. This stops us from using the same number twice in one ordering.

used[i] = True and path.append(nums[i]) are the choose step. We mark the number taken and add it to the path.

backtrack() is the explore step. It fills the next position.

path.pop() and used[i] = False are the un-choose step. After the branch returns, we remove the number and mark it free again. This frees the number so other branches can place it in a different position. Forget the used[i] = False line and your later branches would think the number is still taken.

⏱️ Time and Space Complexity

There are n factorial permutations of n distinct numbers, written n!. So just listing them is already that many. Backtracking builds each one and copies it, which costs n per copy. So the total time is O(n times n!). The extra space, not counting the output, is O(n) for the recursion depth, the path, and the used array.

Approach Time Complexity Space Complexity
Swap in place (alternative) O(n Β· n!) O(n) depth, no used array
Backtracking with used array O(n Β· n!) O(n) extra

Tip

Subsets loops from a moving start index. Permutations loops from 0 every time and uses a used array. That single difference separates combinations from orderings.

🧩 Key Takeaways

  • βœ… A permutation is an ordering that uses every number exactly once.
  • βœ… Order matters here, so [1,2,3] and [2,1,3] are different answers.
  • βœ… Use a used array to track which numbers are already in the path.
  • βœ… The loop starts at 0 every level, not from a moving start index.
  • βœ… The un-choose step must reset both the path and the used flag.

Check Your Knowledge

4 questions Show quiz Hide quiz

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

  1. 1

    How is Permutations different from Subsets?

    Why: A permutation is a full ordering of all numbers, and order matters, so [1,2,3] and [2,1,3] differ.

  2. 2

    What is the role of the used array?

    Why: The used array marks numbers already placed, so the loop can skip them and avoid reusing a number.

  3. 3

    Why does the loop start at index 0 every level instead of a moving start?

    Why: Orderings can use earlier numbers later, so we look at every index each level and rely on the used flags.

  4. 4

    What is the time complexity of generating all permutations of n numbers?

    Why: There are n! permutations and copying each costs n, giving O(n Β· n!) total.

πŸš€ What’s Next?