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
startpointer. 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.
What you’ll learn
Section titled “What you’ll learn”- The
usedarray template, and why astartindex cannot work here. - The duplicate-skip rule for permutations of a multiset — and why it looks wrong at first glance.
- Why output means you cannot beat exponential time, and what that implies.
- Next permutation: the pivot-swap-reverse algorithm in and space.
- Three real LeetCode problems solved in the browser: 46, 47, 31.
The cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”The swap-based version, which avoids copying a list per branch. Watch the array get restored on every return:
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).
The template
Section titled “The template”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:
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 outIt 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.
Handling duplicates
Section titled “Handling duplicates”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:
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 outDry run
Section titled “Dry run”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).
| depth | index | action | path | why |
|---|---|---|---|---|
| 0 | 0 | take 1ₐ | [1] | first copy of the run — always allowed |
| 1 | 1 | take 1♭ | [1,1] | used[0] is True, so the run continues in canonical order |
| 2 | 2 | take 2 | [1,1,2] | emit [1,1,2] |
| 1 | 2 | take 2 | [1,2] | back at depth 1, try the other branch |
| 2 | 1 | take 1♭ | [1,2,1] | emit [1,2,1] |
| 0 | 1 | skip 1♭ | [] | nums[1] == nums[0] and used[0] is False — starting a path with the second one would duplicate the branch we just finished |
| 0 | 2 | take 2 | [2] | |
| 1 | 0 | take 1ₐ | [2,1] | |
| 2 | 1 | take 1♭ | [2,1,1] | emit [2,1,1] |
| 1 | 1 | skip 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 when1ₐ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 thereused[0]wasTrue. 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.
Complexity, and what it implies
Section titled “Complexity, and what it implies”| Time | Space | |
|---|---|---|
| All permutations (46) | recursion, output | |
| Unique permutations (47) | worst case, far less with duplicates | |
| Next permutation (31) | ||
| k-th permutation (60) |
There are permutations and each takes to write down, so
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 without touching the other .
Next permutation — pivot, swap, reverse
Section titled “Next permutation — pivot, swap, reverse”Three steps, and each has a reason:
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 numsThe variant map
Section titled “The variant map”| Variant | The technique | Canonical problem |
|---|---|---|
| All permutations | used array, loop from 0 | 46 |
| With duplicates | Sort + skip when not used[i-1] | 47 |
| In place, no extra array | Swap-based recursion | 46 (alternative) |
| Next in lexicographic order | Pivot, swap, reverse | 31 |
| Previous permutation | Mirror every comparison | 556-adjacent |
| k-th permutation directly | Factorial number system | 60 |
| Permutations of a string with constraints | Same template, prune early | 784 · 267 |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”LC 46 — Permutations · Medium
Section titled “LC 46 — Permutations · Medium”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 — optimal, since there are outputs of length n.
Space 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
usedarray.” 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 ; you cannot do better than producing it.
LC 47 — Permutations II · Medium
Section titled “LC 47 — Permutations II · Medium”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 worst case (all distinct); far fewer branches when duplicates are present, since the skip prunes whole subtrees. Space .
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 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?” — for value multiplicities c_i.
“Same idea for subsets (LC 90)?” — yes, the identical skip rule.
LC 31 — Next Permutation · Medium
Section titled “LC 31 — Next Permutation · Medium”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 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 — three linear scans. Space , 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, soi == -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; thenums[j] <= nums[i]comparison must be non-strict when scanning forjso 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 -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 and not ?” — no
sorting is needed; the suffix is already sorted descending by construction.
LeetCode problem set
Section titled “LeetCode problem set”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.
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.
- 31Next PermutationmediumPivot, swap, reverse -- $O(n)$, $O(1)$ space
- 46Permutationsmedium`used` array, loop from 0 -- no `start` index
- 47Permutations IImediumSort + skip when `not used[i-1]`
- 556Next Greater Element IIImediumLC 31 on digits, plus a 32-bit overflow check
- 784Letter Case PermutationmediumBinary choice per letter; digits are fixed
- 60Permutation SequencehardFactorial number system -- arithmetic, not enumeration
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
“Why no start index?” | The core distinction | Order matters, so earlier elements must remain reachable; consumption is tracked by used instead |
“Why not used[i-1]?” | Whether you derived it | It forces equal values to be consumed left to right, so each distinct arrangement has exactly one generating path |
“Can you avoid the used array?” | Flexibility | The swap-based recursion; less memory, different order, does not extend cleanly to duplicates |
| “How many unique permutations?” | Combinatorics | over value multiplicities |
| “Why is exponential time acceptable?” | Judgement | The output is items, so is optimal |
| “Get the k-th without enumerating?” | Breadth | Factorial number system: k // (n-1)! picks each digit in turn |
| “Prove next-permutation is correct” | Rigour | The suffix after the pivot is maximal, so the pivot is the rightmost increasable index; then minimise the suffix |
Edge-case checklist
Section titled “Edge-case checklist”- 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 == -1and the whole array reverses. - Fully ascending (LC 31) — only the last two elements swap.
- Duplicates with LC 31 —
[1,1,5]; the<=in thejscan matters. - Negative values — legal in 46/47; nothing assumes positivity.
- Forgetting to reset
used[i]— silently yields too few permutations. - Recording
pathinstead oflist(path)— yields a list of empty lists.
Self-check
Section titled “Self-check”-
Why can't permutations use the `start` index that subsets use?
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.
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.
-
In LC 47, what does the condition `nums[i] == nums[i-1] and not used[i-1]` actually enforce?
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.
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.
-
Is the flipped condition `used[i-1]` wrong?
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.
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.
-
You forget to sort before the duplicate-skip. What happens?
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.
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.
-
Why is `out.append(list(path))` rather than `out.append(path)`?
The single most common backtracking bug, in permutations and everywhere else. The output is a snapshot; the path is a workspace.
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.
-
Constraints say `n <= 8`. What is that telling you?
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.
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.
Recall card
Section titled “Recall card”- Cue — “all orderings / arrangements / permutations”, order matters, every element
used. Tiny constraints (
n <= 8). - Template — a
used[]array, not astartindex: every index is available at every depth. Choose → recurse → un-choose, andout.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 spellingused[i-1]is equally valid, the sort is not optional. - Swap-based variant — one shared array, swap and swap back; extra space instead of .
- Cost — time, and that is optimal: the output alone is 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. time, space, no recursion. - k-th permutation (LC 60) — do not enumerate; use factorial-base arithmetic to pick each digit directly.
- Permutations use a
usedarray and a loop from 0; combinations use astartindex. That is the whole structural difference. - Copy on record, and pair every
choosewith itsun-choose— bothpathandused. - For duplicates: sort, then skip
nums[i]when it equalsnums[i-1]andused[i-1]isFalse. That fixes a left-to-right consumption order within each run, so each arrangement is generated once. ACounterover distinct values is an equally good alternative. - is optimal for enumeration — the output is that large.
- Next permutation is a different problem: pivot, swap, reverse, in and 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.coffeeCtapch.feedbackHeading
pch.feedbackSubheading