task
Given an array of distinct integers, return every possible ordering (permutation) of them.
nums = [1, 2, 3] → [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
how to think
Subsets gave every element exactly one decision (include or skip). Permutations is a different
shape: every element gets used exactly once, but order matters, so at every level of the
recursion you’re not deciding yes/no about one fixed element — you’re choosing which of the
still-available elements goes next. That’s why the branching factor shrinks as you go: level 0
has n choices, level 1 has n - 1 (whichever wasn’t just taken), level 2 has n - 2, and so
on down to 1 — multiply those out and you get n!, which is exactly the number of leaves.
The bookkeeping problem this creates is “which elements are still available,” and the clean fix
is a used[] array parallel to nums: before choosing nums[i], check used[i]; after
choosing it, flip it on; after backtracking out of that choice, flip it back off. The un-choose
step now has two parts instead of one — pop the path and free the slot — but each is still a
single, cheap, exactly-reversible line.
template instance
Pick from remaining pool shape: at every level the loop scans all n original elements
but skips any with used[i] == true. Invariant: path is a permutation-prefix of exactly the
elements marked used; when path.Count == nums.Length, every element has been placed exactly
once.
solution
public IList<IList<int>> Permute(int[] nums)
{
var result = new List<IList<int>>();
var path = new List<int>();
var used = new bool[nums.Length];
void Backtrack()
{
if (path.Count == nums.Length)
{
result.Add(new List<int>(path)); // COPY — path keeps mutating after this
return;
}
for (int i = 0; i < nums.Length; i++)
{
if (used[i]) continue; // this element is already somewhere in path
used[i] = true;
path.Add(nums[i]); // choose
Backtrack(); // explore
path.RemoveAt(path.Count - 1); // un-choose (path)
used[i] = false; // un-choose (availability)
}
}
Backtrack();
return result;
}
trace
nums = [1, 2, 3] — branching factor 3, 2, 1 per level, 3! = 6 leaves:
()
├─ choose 1 → (1)
│ ├─ choose 2 → (1,2) ── choose 3 → (1,2,3) *complete*
│ └─ choose 3 → (1,3) ── choose 2 → (1,3,2) *complete*
├─ choose 2 → (2)
│ ├─ choose 1 → (2,1) ── choose 3 → (2,1,3) *complete*
│ └─ choose 3 → (2,3) ── choose 1 → (2,3,1) *complete*
└─ choose 3 → (3)
├─ choose 1 → (3,1) ── choose 2 → (3,1,2) *complete*
└─ choose 2 → (3,2) ── choose 1 → (3,2,1) *complete*
Every choose event, in call order:
| call | depth before | i |
value chosen | path after choose | result |
|---|---|---|---|---|---|
| 1 | 0 | 0 | 1 | [1] |
recurse |
| 2 | 1 | 1 | 2 | [1,2] |
recurse |
| 3 | 2 | 2 | 3 | [1,2,3] |
complete → add [1,2,3] |
| 4 | 1 | 2 | 3 | [1,3] |
recurse (2 was un-chosen first) |
| 5 | 2 | 1 | 2 | [1,3,2] |
complete → add [1,3,2]; unwind to () |
| 6 | 0 | 1 | 2 | [2] |
recurse |
| 7 | 1 | 0 | 1 | [2,1] |
recurse |
| 8 | 2 | 2 | 3 | [2,1,3] |
complete → add [2,1,3] |
| 9 | 1 | 2 | 3 | [2,3] |
recurse |
| 10 | 2 | 0 | 1 | [2,3,1] |
complete → add [2,3,1]; unwind to () |
| 11 | 0 | 2 | 3 | [3] |
recurse |
| 12 | 1 | 0 | 1 | [3,1] |
recurse |
| 13 | 2 | 1 | 2 | [3,1,2] |
complete → add [3,1,2] |
| 14 | 1 | 1 | 2 | [3,2] |
recurse |
| 15 | 2 | 0 | 1 | [3,2,1] |
complete → add [3,2,1]; done |
Call 4 is the row that shows used[] earning its keep: after call 3 adds [1,2,3], the
unwind clears used[2] then used[1] (in that order) as the recursion returns to depth 1 with
path = [1] — used[0] stays true the whole time, since we’re still inside 1’s subtree.
Back at depth 1, the for loop simply continues from where it left off, i = 2, and nums[2] = 3
is available again exactly because its used flag was just cleared.
why it works
Induction on remaining depth. At depth d, exactly n - d elements have used[i] == false;
the loop tries each one, and because used/path are restored to their depth-d state before
trying the next i, every branch at that level sees the same pool of n - d candidates. By
induction each of those branches correctly enumerates every ordering of the remaining n - d - 1
elements, so the whole tree enumerates every ordering of all n — no repeats, because used[]
forbids picking the same index twice on one path, and none missing, because every unused index
gets its turn in the loop.
common bugs
- Forgetting
used[i] = falseon the way back — once an element is marked used it never frees up again, and every branch after the first returns early with fewer thann!results. - Checking
nums[i] == nums[path[^1]]or similar value-based dedup by accident when the array is stated to be distinct — that’s the fix for Permutations II, and applying it here silently drops valid permutations. - Using an index-based loop over
numsdirectly (for (int i = start; ...), Combination Sum’s shape) instead of scanning allnand skippingused[i]— order matters here, so a later element sometimes needs to come before an earlier one, which astart-index loop can’t do. - Copying
pathintoresultbefore the base case triggers — the record-at-every-node mistake, adding every prefix as a “permutation” instead of only the complete ones atpath.Count == nums.Length.
variants you can now solve
- Permutations II (LC 47) — duplicates in the input; sort first, then skip
nums[i]when it equalsnums[i - 1]andnums[i - 1]is currently unused (the “unused” condition is what prevents skipping a legitimately-needed duplicate). - Subsets (LC 78) — the size-agnostic sibling: instead of placing every element exactly once, each one gets a binary in/out decision.
- N-Queens (LC 51) — pick-from-remaining-pool over board columns instead of array values,
with
IsValidchecking diagonals and rows instead of aused[]flag.