Skip to content

Permutations and Arrangements

Subsets and combinations use a start index, because order does not matter — once you have passed an element you never look back. Permutations are different: every element is available at every depth, just not twice on the same path.

That one change drives everything on this page:

Combinations advance a start pointer. Permutations track which elements are already in use.

The second half of the page is a different problem entirely: computing the next permutation in lexicographic order, in place, without generating any of the others. That algorithm is short, non-obvious, and asked constantly.

  • The used array template, and why a start index cannot work here.
  • The duplicate-skip rule for permutations of a multiset — and why it looks wrong at first glance.
  • Why n!n! output means you cannot beat exponential time, and what that implies.
  • Next permutation: the pivot-swap-reverse algorithm in O(n)O(n) and O(1)O(1) space.
  • Three real LeetCode problems solved in the browser: 46, 47, 31.

The swap-based version, which avoids copying a list per branch. Watch the array get restored on every return:

recursionFix one position at a time — the branching factor shrinks by one per levelLC 46 · n! leaves
123123123123132132213213213231231321321321312312
call stack
123
start0array1,2,3
callFix position 0 first: each of the 3 elements gets a turn there, and each choice spawns a subtree over the remaining positions.
1/39

The swap-back on the way out is what lets a single shared array serve the whole tree. Building a fresh list per branch also works and is easier to reason about, but costs O(n · n!) space instead of O(n).

permutations.py
def permute(nums):
    out, path = [], []
    used = [False] * len(nums)
 
    def backtrack():
        if len(path) == len(nums):
            out.append(list(path))          # COPY -- path keeps mutating
            return
        for i in range(len(nums)):          # every index, every time
            if used[i]:
                continue
            used[i] = True                  # choose
            path.append(nums[i])
            backtrack()                     # explore
            path.pop()                      # un-choose
            used[i] = False
 
    backtrack()
    return out
 
 
print(permute([1, 2, 3]))
# [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

The loop starts at 0 every time — that is the difference from combinations. The used array is what prevents reusing an element already on the current path.

An alternative worth knowing is the swap-based version, which permutes the array in place and needs no used array:

permute_swap.py
def permute_swap(nums):
    out = []
 
    def backtrack(start):
        if start == len(nums):
            out.append(list(nums))
            return
        for i in range(start, len(nums)):
            nums[start], nums[i] = nums[i], nums[start]   # choose
            backtrack(start + 1)
            nums[start], nums[i] = nums[i], nums[start]   # un-choose
    backtrack(0)
    return out

It uses less memory, but produces permutations in a different (non-lexicographic) order, and it does not extend cleanly to the duplicate case — which is why the used template is the one to default to.

For [1, 1, 2] the plain template produces [1,1,2] twice, because the two 1s are distinct positions even though they are equal values. Sorting first, then skipping, fixes it:

permutations_ii.py
def permute_unique(nums):
    nums.sort()                              # sorting groups equal values
    out, path = [], []
    used = [False] * len(nums)
 
    def backtrack():
        if len(path) == len(nums):
            out.append(list(path))
            return
        for i in range(len(nums)):
            if used[i]:
                continue
            # skip this duplicate unless its identical predecessor is in use
            if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
                continue
            used[i] = True
            path.append(nums[i])
            backtrack()
            path.pop()
            used[i] = False
 
    backtrack()
    return out

LC 47 — permute_unique([1, 1, 2]), which is the only trace worth walking here because the plain template is unsurprising and the duplicate rule is not. After sorting, nums = [1, 1, 2]; call the two ones 1ₐ (index 0) and 1 (index 1).

depthindexactionpathwhy
00take 1ₐ[1]first copy of the run — always allowed
11take 1[1,1]used[0] is True, so the run continues in canonical order
22take 2[1,1,2]emit [1,1,2]
12take 2[1,2]back at depth 1, try the other branch
21take 1[1,2,1]emit [1,2,1]
01skip 1[]nums[1] == nums[0] and used[0] is False — starting a path with the second one would duplicate the branch we just finished
02take 2[2]
10take 1ₐ[2,1]
21take 1[2,1,1]emit [2,1,1]
11skip 1[2]same rule, one level down: 1ₐ is not in use here

Three permutations, not six. What the trace shows that the condition does not:

  • The two skips are the entire mechanism, and both happen at the start of a run, never in the middle. 1 is skipped exactly when 1ₐ is unused, which means “the first copy is still available, so this branch is a relabelling of one we already explored”.
  • [1,1,2] at depth 1 was allowed even though it also uses two equal values — because there used[0] was True. The rule does not forbid repeated values; it forbids consuming them out of order. That distinction is why the condition reads backwards on first sight.
  • Without sorting, neither skip fires. For the input [1, 2, 1] the equal values are not adjacent, nums[i] == nums[i-1] is never true, and the output contains [1,1,2] twice. The sort is load-bearing, not tidying.
  • Inverting the condition to used[i-1] is equally correct — it fixes the opposite consumption order within each run, and on this input the two skips simply land on different branches. What breaks is dropping the sort: for [1, 2, 1] the equal values are not adjacent, no skip ever fires, and every distinct arrangement is emitted twice.
TimeSpace
All permutations (46)O(nn!)O(n \cdot n!)O(n)O(n) recursion, O(nn!)O(n \cdot n!) output
Unique permutations (47)O(nn!)O(n \cdot n!) worst case, far less with duplicatesO(n)O(n)
Next permutation (31)O(n)O(n)O(1)O(1)
k-th permutation (60)O(n2)O(n^2)O(n)O(n)

There are n!n! permutations and each takes O(n)O(n) to write down, so O(nn!)O(n \cdot n!) is optimal — you cannot beat the size of the output. That is why LC 46’s constraint is n <= 6: the problem is not asking you to be clever about complexity, it is asking whether you can enumerate correctly.

Which makes LC 31 the interesting one: it produces one permutation in O(n)O(n) without touching the other n!1n! - 1.

Three steps, and each has a reason:

next_permutation.py
def next_permutation(nums):
    # 1. find the rightmost position that can be increased
    i = len(nums) - 2
    while i >= 0 and nums[i] >= nums[i + 1]:
        i -= 1
 
    # 2. swap it with the smallest value to its right that still exceeds it
    if i >= 0:
        j = len(nums) - 1
        while nums[j] <= nums[i]:
            j -= 1
        nums[i], nums[j] = nums[j], nums[i]
 
    # 3. the suffix was descending (maximal); reverse it to make it minimal
    nums[i + 1:] = reversed(nums[i + 1:])
    return nums
VariantThe techniqueCanonical problem
All permutationsused array, loop from 046
With duplicatesSort + skip when not used[i-1]47
In place, no extra arraySwap-based recursion46 (alternative)
Next in lexicographic orderPivot, swap, reverse31
Previous permutationMirror every comparison556-adjacent
k-th permutation directlyFactorial number system60
Permutations of a string with constraintsSame template, prune early784 · 267

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

Constraints. 1 <= len(nums) <= 6, -10 <= nums[i] <= 10, all distinct.

Examples. [1,2,3] gives all 6 permutations · [0,1] gives [[0,1],[1,0]] · [1] gives [[1]]

Editorial — approach, complexity, follow-ups

Standard backtracking. At each depth, try every element not already on the path. When the path reaches full length, record a copy.

Time O(nn!)O(n \cdot n!) — optimal, since there are n!n! outputs of length n. Space O(n)O(n) for the recursion and path, excluding the output.

The structural point worth stating: combinations use a start index, permutations use a used array. With combinations, [1,2] and [2,1] are the same answer, so you only ever move forward. With permutations they are different answers, so you must be able to reach back to earlier elements — hence the loop from 0 and the need to track consumption separately.

Note the three paired lines. Both used[i] and path must be restored, and the most common bug is restoring one but not the other.

Follow-ups you should expect:

  • “With duplicates (LC 47)?” Sort and add the skip rule — next problem.
  • “Do it without the used array.” The swap-based version shown above: swap into position, recurse, swap back. Less memory, different output order, and it does not adapt to duplicates as cleanly.
  • “Iteratively?” Start from [[]] and, for each element, insert it at every position of every partial permutation built so far.
  • “Just the k-th one (LC 60)?” Factorial number system, no enumeration.
  • “Why is exponential acceptable here?” The output itself is n!n!; you cannot do better than producing it.

Problem. Given a collection of numbers that may contain duplicates, return all unique permutations in any order.

Constraints. 1 <= len(nums) <= 8, -10 <= nums[i] <= 10.

Examples. [1,1,2] gives [[1,1,2],[1,2,1],[2,1,1]] — three, not six · [1,2,3] gives all 6

Editorial — approach, complexity, follow-ups

Duplicates create identical permutations from distinct positions. The fix is to impose a canonical order on equal values: within any run, they must be consumed left to right. Then each distinct arrangement has exactly one generating path.

Time O(nn!)O(n \cdot n!) worst case (all distinct); far fewer branches when duplicates are present, since the skip prunes whole subtrees. Space O(n)O(n).

Both parts are essential:

  • Sorting makes equal values adjacent, which is what nums[i] == nums[i-1] relies on. Without it the rule never triggers and duplicates slip through.
  • not used[i - 1] is the correct direction, for the reason derived in the note above. This is the single most-misremembered line in backtracking, and it is worth being able to explain rather than recall.

[1,1] giving exactly [[1,1]] is the minimal test: the naive template returns it twice.

An alternative that avoids the reasoning entirely: generate all permutations and deduplicate with a set of tuples. It is correct, and worth mentioning — but it wastes work generating duplicates only to discard them, and uses O(n!)O(n!) extra memory. The skip rule prunes them before they are built.

Follow-ups you should expect: “Why not used[i-1]?” — the most likely question; derive it. “Could you use a Counter instead?” — yes, and it is arguably cleaner: recurse over distinct values with remaining counts, which needs no sorting and no used array. Worth offering. “How many unique permutations are there?” — n!/(ci!)n! / \prod(c_i!) for value multiplicities c_i. “Same idea for subsets (LC 90)?” — yes, the identical skip rule.

Problem. Rearrange nums into the next lexicographically greater permutation. If no greater permutation exists, rearrange into the lowest (ascending) order. Must be done in place with O(1)O(1) extra memory.

Constraints. 1 <= len(nums) <= 100, 0 <= nums[i] <= 100.

Examples. [1,2,3] gives [1,3,2] · [3,2,1] gives [1,2,3] (wraps) · [1,1,5] gives [1,5,1]

Editorial — approach, complexity, follow-ups

Pivot, swap, reverse. The full justification is in the note above; the summary is that the suffix after the pivot is already maximal, so the pivot is the rightmost place an increase is possible, and after swapping in the smallest larger value the suffix must be minimised.

Time O(n)O(n) — three linear scans. Space O(1)O(1), genuinely in place.

The test cases map onto the distinct behaviours:

  • [1,2,3] gives [1,3,2] — pivot at index 1, the ordinary case.
  • [3,2,1] gives [1,2,3] — fully descending, so i == -1, step 2 is skipped and step 3 reverses everything. The wrap-around falls out of the algorithm with no special case, which is the elegant part.
  • [1,1,5] gives [1,5,1] — duplicates; the nums[j] <= nums[i] comparison must be non-strict when scanning for j so it skips over equal values and finds a genuinely larger one.
  • [2,3,1] gives [3,1,2] — pivot at index 0, and the suffix [3,1] reverses to [1,2] after the swap.

The comparisons in steps 1 and 2 are worth care: step 1 uses >= (skip non-ascending pairs) and step 2 uses <= (skip values not strictly greater). Making either strict or non-strict incorrectly breaks the duplicate cases while leaving the distinct-value cases working — so [1,1,5] is the test that catches it.

Follow-ups you should expect: “Previous permutation?” — mirror every comparison. “Generate all permutations by repeated application?” — start from the sorted array and apply n! times; a neat O(1)O(1)-space enumerator. “Next greater element with the same digits (LC 556)?” — this exact algorithm on the digit array, plus an overflow check. “Why is it O(n)O(n) and not O(nlogn)O(n \log n)?” — no sorting is needed; the suffix is already sorted descending by construction.

Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.

6 problems
0 easy5 medium1 hard

Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.

They askWhat they’re checkingThe answer
“Why no start index?”The core distinctionOrder matters, so earlier elements must remain reachable; consumption is tracked by used instead
“Why not used[i-1]?”Whether you derived itIt forces equal values to be consumed left to right, so each distinct arrangement has exactly one generating path
“Can you avoid the used array?”FlexibilityThe swap-based recursion; less memory, different order, does not extend cleanly to duplicates
“How many unique permutations?”Combinatoricsn!/(ci!)n! / \prod(c_i!) over value multiplicities
“Why is exponential time acceptable?”JudgementThe output is n!n! items, so O(nn!)O(n \cdot n!) is optimal
“Get the k-th without enumerating?”BreadthFactorial number system: k // (n-1)! picks each digit in turn
“Prove next-permutation is correct”RigourThe suffix after the pivot is maximal, so the pivot is the rightmost increasable index; then minimise the suffix
  • Single element — one permutation; LC 31 returns it unchanged.
  • Two elements — the smallest case where ordering is visible.
  • All identical[1,1] gives exactly one unique permutation.
  • Fully descending (LC 31) — [3,2,1]; i == -1 and the whole array reverses.
  • Fully ascending (LC 31) — only the last two elements swap.
  • Duplicates with LC 31[1,1,5]; the <= in the j scan matters.
  • Negative values — legal in 46/47; nothing assumes positivity.
  • Forgetting to reset used[i] — silently yields too few permutations.
  • Recording path instead of list(path) — yields a list of empty lists.
pch.quizTag Permutations — self-check
  1. Why can't permutations use the `start` index that subsets use?

    pch.quizShowAnswer

    B — Because order matters and every element must be used — a start index deliberately forbids revisiting earlier indices, which is exactly what a permutation needs to do — Subsets need each element considered once, so `start` prevents re-picking. Permutations need every index available at every depth, which is what the `used` array provides.

  2. In LC 47, what does the condition `nums[i] == nums[i-1] and not used[i-1]` actually enforce?

    pch.quizShowAnswer

    B — That within a run of equal values, they are consumed in a fixed order — so the second copy may not start a branch while the first is still unused — It does not forbid repeated values — [1,1,2] is a valid output. It forbids consuming them out of order, because that branch is a relabelling of one already explored.

  3. Is the flipped condition `used[i-1]` wrong?

    pch.quizShowAnswer

    B — No — it fixes the opposite consumption order within each run and is equally correct; what is mandatory is the sort plus the equality guard, not a particular spelling of the boolean — Verified against itertools.permutations on several multisets: both spellings emit exactly the distinct arrangements. Remembering the *reason* (fix one order per run) beats memorising the boolean.

  4. You forget to sort before the duplicate-skip. What happens?

    pch.quizShowAnswer

    B — Equal values are no longer adjacent, so `nums[i] == nums[i-1]` never fires and duplicates come through — `[1,2,1]` emits each distinct arrangement twice — The skip rule is entirely positional — it compares neighbours. Sorting is what makes 'neighbour' mean 'same value', so it is part of the algorithm rather than presentation.

  5. Why is `out.append(list(path))` rather than `out.append(path)`?

    pch.quizShowAnswer

    B — Because `path` is one list mutated throughout the traversal — storing the reference means every recorded permutation is the same object, which the un-choose `pop()` then empties — The single most common backtracking bug, in permutations and everywhere else. The output is a snapshot; the path is a workspace.

  6. Constraints say `n <= 8`. What is that telling you?

    pch.quizShowAnswer

    B — That factorial output is expected and acceptable — 8! is 40,320, and there is no way to beat exponential time when the answer itself has n! entries — You cannot enumerate n! things in less than O(n!) time. Tiny constraints are the signal that enumeration is the intended solution — which also means the k-th permutation (LC 60) must NOT enumerate.

  • Cue — “all orderings / arrangements / permutations”, order matters, every element used. Tiny constraints (n <= 8).
  • Template — a used[] array, not a start index: every index is available at every depth. Choose → recurse → un-choose, and out.append(list(path)) — the copy is mandatory.
  • Duplicates (LC 47)sort, then skip nums[i] == nums[i-1] and not used[i-1]. The rule is “fix one consumption order within each run of equal values”; the mirrored spelling used[i-1] is equally valid, the sort is not optional.
  • Swap-based variant — one shared array, swap and swap back; O(n)O(n) extra space instead of O(nn!)O(n \cdot n!).
  • CostO(nn!)O(n \cdot n!) time, and that is optimal: the output alone is n!n! entries.
  • Next permutation (LC 31) — find the rightmost a[i] < a[i+1] (the pivot), swap with the rightmost value greater than it, reverse the suffix. O(n)O(n) time, O(1)O(1) space, no recursion.
  • k-th permutation (LC 60) — do not enumerate; use factorial-base arithmetic to pick each digit directly.
  • Permutations use a used array and a loop from 0; combinations use a start index. That is the whole structural difference.
  • Copy on record, and pair every choose with its un-choose — both path and used.
  • For duplicates: sort, then skip nums[i] when it equals nums[i-1] and used[i-1] is False. That fixes a left-to-right consumption order within each run, so each arrangement is generated once. A Counter over distinct values is an equally good alternative.
  • O(nn!)O(n \cdot n!) is optimal for enumeration — the output is that large.
  • Next permutation is a different problem: pivot, swap, reverse, in O(n)O(n) and O(1)O(1) space, with the wrap-around handled automatically.
  • “Give me the k-th” is usually arithmetic on the counting structure, not enumeration.

Next: Backtracking — the same skeleton applied to constrained search, where pruning is what makes it feasible.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading