~/DHRUVUpskilling
← board/DSA/Subsets/dsa-subsets-02
Solved·28 Sept

Permutation

DifficultyMedium
PatternSubsets
TrackDSA
tl;dr

Return all permutation of a number

full write-up

Permutations — Backtracking in JavaScript

Problem

Given an array of distinct integers, return all possible permutations.

Example:

Input:
[1, 2, 3]

Output:
[
  [1, 2, 3],
  [1, 3, 2],
  [2, 1, 3],
  [2, 3, 1],
  [3, 2, 1],
  [3, 1, 2]
]

The Solution

var permute = function (nums) {
    var result = [];

    function generatePermute(start) {

        // We have filled all positions
        if (start === nums.length - 1) {
            result.push([...nums]);
            return;
        }

        // Try every remaining number at position `start`
        for (let i = start; i < nums.length; i++) {

            // Choose
            [nums[start], nums[i]] = [nums[i], nums[start]];

            // Explore
            generatePermute(start + 1);

            // Undo the choice
            [nums[start], nums[i]] = [nums[i], nums[start]];
        }
    }

    generatePermute(0);

    return result;
};

Main Idea

The most important idea is:

At every position, try every number that is still available.

For example:

[1, 2, 3]

Position 0:
    Try 1
    Try 2
    Try 3

If we choose 1 for position 0:

[1, 2, 3]
    ↓
Position 0 is fixed

[1, ?, ?]

Now we solve the remaining positions:

Position 1:
    Try 2
    Try 3

This gives:

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

Then we go back and try another number for position 0.

What Does start Mean?

start tells us:

Which position are we currently trying to fill?

For example:

[1, 2, 3]
 ↑
 start = 0

We are deciding what should go at index 0.

After choosing something:

generatePermute(start + 1);

we move to the next position:

[1, 2, 3]
    ↑
  start = 1

Now index 0 is already fixed.

So:

start = 0

[?, ?, ?]


start = 1

[1, ?, ?]


start = 2

[1, 2, ?]

Why Do We Swap?

This line:

[nums[start], nums[i]] = [nums[i], nums[start]];

puts the number at i into the position we're currently trying to fill.

Suppose:

nums = [1, 2, 3]

start = 0
i = 1

We want to try 2 at index 0.

Before:

[1, 2, 3]
 ↑  ↑
start i

Swap:

[nums[0], nums[1]] = [nums[1], nums[0]];

After:

[2, 1, 3]
 ↑
start

Now 2 has been chosen for position 0.


Then We Recurse

After making the choice:

generatePermute(start + 1);

We move to the next position.

So:

[2, 1, 3]
    ↑
  start = 1

Now we decide what goes at index 1.

We can choose:

1

or:

3

Giving:

[2, 1, 3]

[2, 3, 1]

Why Do We Swap Back?

This is the most important part of backtracking.

After exploring a choice:

[nums[start], nums[i]] = [nums[i], nums[start]];

we undo the swap.

For example:

Original:

[1, 2, 3]

Choose 2:

[2, 1, 3]

Explore everything starting with 2.

After we're finished:

[2, 1, 3]

Swap back:

[1, 2, 3]

Now we can try choosing 3.

[3, 2, 1]

Backtracking Pattern

The general pattern is:

// Choose
make a change

// Explore
recursive call

// Undo
reverse the change

In this problem:

// Choose
[nums[start], nums[i]] = [nums[i], nums[start]];

// Explore
generatePermute(start + 1);

// Undo
[nums[start], nums[i]] = [nums[i], nums[start]];

Think:

        CHOOSE
           ↓
        EXPLORE
           ↓
         UNDO
           ↓
     Try next choice

This is the essence of backtracking.


Step-by-Step Example

Let's use:

nums = [1, 2, 3]

Step 1

Call:

generatePermute(0)

We need to decide index 0.

The loop is:

for (let i = 0; i < nums.length; i++)

So we try:

i = 0
i = 1
i = 2

Step 2 — Choose 1

i = 0

[1, 2, 3]
 ↑
start

Swap:

[1, 2, 3]

Then:

generatePermute(1)

Step 3 — Choose 2

Now:

[1, 2, 3]
    ↑
  start

Try i = 1.

Swap:

[1, 2, 3]

Then:

generatePermute(2)

Step 4 — Base Case

Now:

start === nums.length - 1

is true:

2 === 3 - 1

So we have a complete permutation.

result.push([...nums]);

We add:

[1, 2, 3]

to the result.


Why [...nums]?

This is important.

We don't do:

result.push(nums);

because nums is continuously modified by swapping.

Instead:

result.push([...nums]);

creates a copy.

For example:

let nums = [1, 2, 3];

let copy = [...nums];

Now:

nums = [1, 2, 3]
copy = [1, 2, 3]

If we later change nums:

nums = [2, 1, 3]
copy = [1, 2, 3]

The saved permutation remains unchanged.


Step 5 — Backtrack

After finding:

[1, 2, 3]

we return from:

generatePermute(2);

Then we undo our previous choice.

The array returns to:

[1, 2, 3]

Now at start = 1, the loop continues.


Step 6 — Choose 3

At:

[1, 2, 3]
    ↑
  start

now:

i = 2

Swap index 1 and 2:

[1, 3, 2]

Then:

generatePermute(2);

Base case is reached.

Add:

[1, 3, 2]

Now:

result = [
    [1, 2, 3],
    [1, 3, 2]
];

Then swap back:

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

Step 7 — Back to Position 0

We've finished every permutation starting with 1.

Now the loop at start = 0 continues:

i = 1

Swap:

[1, 2, 3]
 ↑  ↑
 0  1

becomes:

[2, 1, 3]

Now index 0 is fixed as 2.

Then:

generatePermute(1);

This produces:

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

Then we backtrack again.


Finally Choose 3

At start = 0:

i = 2

Swap:

[1, 2, 3]
       ↑

becomes:

[3, 2, 1]

Then recursively generate the remaining possibilities:

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

Final Result

The recursion eventually produces:

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

Visualizing the Recursion Tree

The easiest way to understand the recursion is as a tree:

                         []
                    /     |     \
                   1      2      3
                  / \    / \    / \
                 2   3  1   3  2   1
                 |   |  |   |  |   |
                 3   2  3   1  1   2

Reading from top to bottom gives:

1 → 2 → 3 = [1,2,3]
1 → 3 → 2 = [1,3,2]

2 → 1 → 3 = [2,1,3]
2 → 3 → 1 = [2,3,1]

3 → 2 → 1 = [3,2,1]
3 → 1 → 2 = [3,1,2]

Every path from the root to a leaf is one permutation.


What Exactly Happens in the Loop?

This:

for (let i = start; i < nums.length; i++) {
    [nums[start], nums[i]] = [nums[i], nums[start]];

    generatePermute(start + 1);

    [nums[start], nums[i]] = [nums[i], nums[start]];
}

can be understood as:

For the current position:

    Try the number at index start
        ↓
    Explore all permutations

    Undo

    Try the next number
        ↓
    Explore all permutations

    Undo

    Try the next number
        ↓
    Explore all permutations

    Undo

Why Does i Start at start?

Notice:

for (let i = start; i < nums.length; i++)

not:

for (let i = 0; i < nums.length; i++)

Why?

Because everything before start is already fixed.

For example:

[2, 1, 3]
 ↑  ↑  ↑
 |  |  |
 |  |  current position
 |  already fixed
 already fixed

If:

start = 2

we only care about index 2.

We don't want to touch:

index 0
index 1

because those positions have already been chosen.

So:

i = start

means:

"Only consider numbers that haven't already been fixed."


The Base Case

if (start === nums.length - 1) {
    result.push([...nums]);
    return;
}

When start reaches the final index, every previous position has already been chosen.

For:

[1, 2, 3]

when:

start = 2

we have:

index 0 → chosen
index 1 → chosen
index 2 → automatically remaining

Therefore:

[1, 2, 3]

is a complete permutation.

We save it and return.


A Mental Model

Don't think of the code as:

"I'm swapping random elements."

Instead think:

"I'm choosing which number belongs at the current position."

For example:

start = 0

Which number should be at position 0?

    1
    2
    3

Choose 1.

[1, ?, ?]

Then:

start = 1

Which number should be at position 1?

    2
    3

Choose 2.

[1, 2, ?]

The remaining number must be 3.

[1, 2, 3]

Save it.

Then backtrack and try another choice.


The Three Important Operations

Remember these three words:

1. CHOOSE
2. EXPLORE
3. UNDO

In code:

// CHOOSE
[nums[start], nums[i]] = [nums[i], nums[start]];

// EXPLORE
generatePermute(start + 1);

// UNDO
[nums[start], nums[i]] = [nums[i], nums[start]];

This pattern appears in many backtracking problems.

Examples:

  • Permutations
  • Subsets
  • Combination Sum
  • N-Queens
  • Sudoku
  • Word Search
  • Generate Parentheses

Complexity

For n numbers, there are:

n!

possible permutations.

For:

[1,2,3]

there are:

3! = 6

For:

[1,2,3,4]

there are:

4! = 24

For:

[1,2,3,4,5]

there are:

5! = 120

So the time complexity is approximately:

O(n × n!)

because there are n! permutations and copying each permutation takes O(n).

The result itself requires:

O(n × n!)

space because we store all permutations.


One-Sentence Summary

The algorithm repeatedly:

Choose a number for the current posi
Permutation — pattern notes — Dhruv Parmar