AlgoInsight

Free lesson · DSA Mastery - Interview Prep, End to End

Permutations

Medium7 min readdsarecursion-backtracking

Permutations

Permutations changes the question from which elements to choose to in what order to arrange them. Generate all orderings of a list of distinct numbers. The structural shift from the combination problems is significant: permutations care about position, so instead of a start index that only moves forward (which deliberately ignores order), you track which elements are used and may pick any unused element at each step - producing n! orderings instead of 2^n subsets. This is the canonical "track used elements" backtracking pattern, and it's the foundation for the two permutation variants that follow. We present two equivalent approaches: a used[] boolean array, and an in-place swapping method.

The insight: a permutation uses every element exactly once, in some order. At each position you choose any element not yet placed. Where combinations forbid going backward (to ignore order), permutations allow picking any unused element (because order is exactly what matters) - so the mechanism is a used marker, not a start index.

Interactive animation: Step through Permutations yourself: every approach, frame by frame, on the lesson's own examples.

The Problem

Interactive animation: The lesson's example: the input, and the answer the algorithm has to produce.

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

Example 1:

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

Example 2:

Input:  nums = [0,1]
Output: [[0,1],[1,0]]

Example 3:

Input:  nums = [1]
Output: [[1]]

Constraints:

Approaches Overview

# Approach Time Space Verdict
1 Backtracking with used[] array O(n · n!) O(n) Clearest pattern
2 Backtracking with in-place swaps O(n · n!) O(n) No extra array; reorders in place

Both generate all n! permutations (each of length n to copy, hence n · n!). The used[] version makes the "pick any unused element" idea explicit; the swapping version fixes one position at a time by swapping candidates into place, using no auxiliary array.

Approach 1: Backtracking with used[] Array - Clearest

The Idea

Maintain a used[] boolean array and a path. Success: when path.size() == n, record. Otherwise loop over all indices i; skip if used[i]; otherwise mark used[i] = true, add nums[i], recurse, then undo both. Because every index is considered at every position (not just those after a start), all n! orderings are produced.

Logic Flowchart

graph TD
    A["backtrack(path, used)"] --> B{"path.size() == n?"}
    B -->|Yes| C["record copy of path; return"]
    B -->|No| D["for i = 0 .. n-1"]
    D --> E{"used[i]?"}
    E -->|Yes| F["skip"]
    E -->|No| G["used[i]=true; path.add(nums[i])"]
    G --> H["backtrack(path, used)"]
    H --> I["path.remove(last); used[i]=false - undo"]
    F --> D
    I --> D

    style A fill:#4338ca,stroke:#6366f1,color:#f1f5f9
    style B fill:#b45309,stroke:#f59e0b,color:#f1f5f9
    style C fill:#16a34a,stroke:#10b981,color:#f1f5f9
    style D fill:#4338ca,stroke:#6366f1,color:#f1f5f9
    style E fill:#b45309,stroke:#f59e0b,color:#f1f5f9
    style F fill:#dc2626,stroke:#ef4444,color:#f1f5f9
    style G fill:#16a34a,stroke:#10b981,color:#f1f5f9
    style H fill:#4338ca,stroke:#6366f1,color:#f1f5f9
    style I fill:#dc2626,stroke:#ef4444,color:#f1f5f9
This flowchart shows permutations with a used array. When the path is full we record it. Otherwise we consider every index, skip those already used, and for each unused index mark it used, add its value, recurse, then unmark and remove to undo. Considering all indices each step yields every ordering.

Code

public static List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(nums, new boolean[nums.length], new ArrayList<>(), result);
    return result;
}

private static void backtrack(int[] nums, boolean[] used,
                              List<Integer> path, List<List<Integer>> result) {
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));    // full ordering
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;                 // already placed
        used[i] = true;                        // choose nums[i] for this position
        path.add(nums[i]);
        backtrack(nums, used, path, result);
        path.remove(path.size() - 1);          // undo value
        used[i] = false;                       // undo used marker
    }
}

// Usage:
// permute([1,2,3]) -> all 6 orderings
This Java code uses a boolean used array. When the path holds all elements it records the permutation. Otherwise it tries every unused index, marks it used, adds its value, recurses, then unmarks and removes to restore state.

Step-by-Step Walkthrough

A frame-by-frame trace on nums = [1,2,3]:

graph LR
    F0["pos0 pick 1; pos1 pick 2; pos2 pick 3 → [1,2,3]"]
    F1["undo: pos1 pick 3; pos2 pick 2 → [1,3,2]"]
    F2["undo to pos0: pick 2 → [2,1,3],[2,3,1]"]
    F3["pick 3 → [3,1,2],[3,2,1]"]
    F4["6 permutations"]
    F0 --> F1 --> F2 --> F3 --> F4

    style F0 fill:#16a34a,stroke:#10b981,color:#f1f5f9
    style F1 fill:#16a34a,stroke:#10b981,color:#f1f5f9
    style F2 fill:#16a34a,stroke:#10b981,color:#f1f5f9
    style F3 fill:#16a34a,stroke:#10b981,color:#f1f5f9
    style F4 fill:#16a34a,stroke:#10b981,color:#f1f5f9
This walkthrough permutes one, two, three. Fixing the first element as one yields one-two-three and one-three-two; as two yields two-one-three and two-three-one; as three yields three-one-two and three-two-one - six orderings in all.

Result: all 6 permutations of [1,2,3].

Interactive animation: Every level looks at every index.

Complexity

Time: O(n · n!) - n! permutations, each O(n) to copy. Space: O(n) recursion depth + O(n) used.

Approach 2: Backtracking with In-Place Swaps - No Extra Array

The Idea

Fix positions left to right. At position start, swap each candidate index i (from start to n-1) into start, recurse on start + 1, then swap back (undo). The prefix [0, start) holds the fixed elements; the suffix is the pool of remaining candidates. No used array is needed - the swap itself tracks placement, and the array is the working state.

Logic Flowchart

graph TD
    A["permute(start)"] --> B{"start == n?"}
    B -->|Yes| C["record copy of nums; return"]
    B -->|No| D["for i = start .. n-1"]
    D --> E["swap(start, i)"]
    E --> F["permute(start + 1)"]
    F --> G["swap(start, i) - undo"]
    G --> H{"more i?"}
    H -->|Yes| D
    H -->|No| I["return"]

    style A fill:#4338ca,stroke:#6366f1,color:#f1f5f9
    style B fill:#b45309,stroke:#f59e0b,color:#f1f5f9
    style C fill:#16a34a,stroke:#10b981,color:#f1f5f9
    style D fill:#4338ca,stroke:#6366f1,color:#f1f5f9
    style E fill:#16a34a,stroke:#10b981,color:#f1f5f9
    style F fill:#4338ca,stroke:#6366f1,color:#f1f5f9
    style G fill:#dc2626,stroke:#ef4444,color:#f1f5f9
    style H fill:#b45309,stroke:#f59e0b,color:#f1f5f9
    style I fill:#16a34a,stroke:#10b981,color:#f1f5f9
This flowchart shows the in-place swap method. To fill a position, each candidate from that position onward is swapped into place, the next position is filled recursively, then the swap is undone. When all positions are fixed, the array is recorded.

Code

public static List<List<Integer>> permuteSwap(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    permuteFrom(nums, 0, result);
    return result;
}

private static void permuteFrom(int[] nums, int start, List<List<Integer>> result) {
    if (start == nums.length) {
        List<Integer> perm = new ArrayList<>();
        for (int v : nums) perm.add(v);        // snapshot current arrangement
        result.add(perm);
        return;
    }
    for (int i = start; i < nums.length; i++) {
        swap(nums, start, i);                  // place nums[i] at position `start`
        permuteFrom(nums, start + 1, result);
        swap(nums, start, i);                  // undo
    }
}

private static void swap(int[] a, int x, int y) { int t = a[x]; a[x] = a[y]; a[y] = t; }
This Java code permutes by swapping. For each position it swaps every later candidate into place, recurses to fill the next position, then swaps back. When every position is fixed it snapshots the array as one permutation.

Complexity

Time: O(n · n!). Space: O(n) recursion depth, no auxiliary used array.

Complexity Comparison

Approach Time Space Tracking When to Use
used[] array O(n · n!) O(n) + used Explicit boolean markers Clearest; extends to duplicates
In-place swaps O(n · n!) O(n) Array position itself No extra array; elegant

Both are optimal (output is n!). The used[] version is the better teaching and interview default because it reads directly as "pick any unused element" and extends cleanly to the duplicate variant. The swap version is slick and memory-light but trickier to adapt for duplicate handling.

Interactive animation: Both O(n x n!). The swap version just looks at fewer indexes on the way.

Variations & Follow-ups

Why a used[] array here instead of a start index?

A start index enforces increasing order, which is exactly how combinations ignore order - perfect when order doesn't matter. Permutations need every ordering, so you must be able to pick elements in any sequence, including "earlier" ones. The used[] array lets you choose any not-yet-placed element at each position, generating all n! arrangements.

Why n! permutations versus 2^n subsets?

A subset is n independent include/exclude decisions → 2^n. A permutation fills n positions: n choices for the first, n-1 for the second, and so on → n!. The combinatorics differ because permutations use all elements and care about order, while subsets choose a subset and ignore order.

How does the swap method avoid a used array?

The array is partitioned into a fixed prefix [0, start) and a candidate suffix [start, n). Swapping a suffix element into start "places" it; the recursion then permutes the smaller suffix. Swapping back restores the pool. Placement is encoded in the array layout itself, so no separate marker is needed.

Which is better for the duplicate variant (Permutations II)?

The used[] version, by a wide margin. Handling duplicates requires sorting and a sibling-skip condition (i > 0 && nums[i] == nums[i-1] && !used[i-1]) that's clean with explicit used markers but awkward with swaps (swapping disturbs the sorted order the skip relies on). That's why the next problem builds on the used[] form.

What about edge cases?

Common Interview Pitfalls

Why This Topic Matters

Permutations introduces the "track used elements" backtracking pattern - the counterpart to the combination family's start-index pattern. Knowing when order matters (use used[], consider all elements) versus when it doesn't (use start, go forward only) is one of the most important distinctions in backtracking, and it determines the entire structure of the search.

The next topic, Letter Combinations of a Phone Number, applies permutation-style "pick one per position" backtracking to a mapping problem: each digit maps to a set of letters, and you generate every combination of letters across the digits. It's a different flavor - the choices at each position come from a digit-to-letters table rather than a shared pool - but the choose-one-per-position-and-recurse structure is the same skeleton you just built.

Back to DSA Mastery - Interview Prep, End to End