Permutations (LeetCode 46)
Given an array nums of distinct integers, return every possible ordering of all its elements -- i.e., every permutation -- in any order.
A permutation uses each element from nums exactly once, arranged into some linear sequence.
Worked Examples
nums = [1,2,3][[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]3 * 2 * 1 = 6 ways.nums = [0,1][[0,1],[1,0]]nums = [1][[1]]⚖️Formal Constraints & Bounds
1 <= nums.length <= 6-10 <= nums[i] <= 10All the integers of
numsare unique.
Core Insight
A permutation uses every element, only reordering them -- unlike a subset, which may leave elements out. Because an earlier element may still be needed later, a used set (not a start index) tracks which elements are currently placed; both path and used must be reverted together on backtrack.
Real-World Scenario & Production Applications
Scheduling every possible ordering of a fixed set of tasks on a single machine, or enumerating every seating arrangement at a table, both require generating every permutation of a small fixed set.
Subproblems & Recurrence Decomposition3 Phases
Iterate through all elements in nums and branch on candidates that have not yet been used in the active permutation (not used[i]).
for i in range(len(nums)):
if not used[i]:Step-by-Step Execution Trace Table
[ ] (Root, used={}, d=0) / | \ choose 1 / choose 2 | choose 3 \ / | \ [ 1 ] (d=1) [ 2 ] (d=1) [ 3 ] (d=1) / \ / \ / \ choose 2/ choose 3 \ choose 1/ choose 3 \ choose 1/ choose 2 \ / \ / \ / \ [1,2] (d=2) [1,3] (d=2)[2,1] (d=2) [2,3] (d=2)[3,1] (d=2) [3,2] (d=2) | | | | | | choose 3| choose 2| choose 3| choose 1| choose 2| choose 1| v v v v v v [1,2,3] ✅ [1,3,2] ✅[2,1,3] ✅ [2,3,1] ✅[3,1,2] ✅ [3,2,1] ✅ Step-by-Step Decision Trace (for nums = [1, 2, 3])
- Step 1 (Root, d=0):
path = [],used = {}. Loop available candidates[1, 2, 3]. - Step 2 (Choose 1, d=1):
path = [1],used = {1}. Loop candidates[1, 2, 3](1in used skip). - Step 3 (Branch Left: Choose 2, d=2):
path = [1, 2],used = {1, 2}. Loop candidates[1, 2, 3](1, 2in used skip). - Step 4 (Choose 3, d=3):
path = [1, 2, 3],used = {1, 2, 3}. Base Case (len == 3): Record[1, 2, 3]✅ (#1). - Step 5 (Backtrack / Pop 3, d=2):
path = [1, 2],used = {1, 2}↩️ BACKTRACK / POP (unmark3). - Step 6 (Backtrack / Pop 2, d=1):
path = [1],used = {1}↩️ BACKTRACK / POP (unmark2). Next candidate:3. - Step 7 (Branch Right: Choose 3, d=2):
path = [1, 3],used = {1, 3}. Loop candidates[1, 2, 3](1, 3in used skip). - Step 8 (Choose 2, d=3):
path = [1, 3, 2],used = {1, 2, 3}. Base Case (len == 3): Record[1, 3, 2]✅ (#2). - Step 9 (Backtrack / Pop 2, d=2):
path = [1, 3],used = {1, 3}↩️ BACKTRACK / POP (unmark2). - Step 10 (Backtrack / Pop 3, d=1):
path = [1],used = {1}↩️ BACKTRACK / POP (unmark3). - Step 11 (Backtrack / Pop 1, d=0):
path = [],used = {}↩️ BACKTRACK / POP to Root (unmark1). Next candidate:2. - Step 12 (Choose 2, d=1):
path = [2],used = {2}. Loop candidates[1, 2, 3](2in used skip). - Step 13 (Branch Left: Choose 1, d=2):
path = [2, 1],used = {1, 2}. Loop candidates[1, 2, 3](1, 2in used skip). - Step 14 (Choose 3, d=3):
path = [2, 1, 3],used = {1, 2, 3}. Base Case (len == 3): Record[2, 1, 3]✅ (#3). - Step 15 (Backtrack / Pop 3, d=2):
path = [2, 1],used = {1, 2}↩️ BACKTRACK / POP (unmark3). - Step 16 (Backtrack / Pop 1, d=1):
path = [2],used = {2}↩️ BACKTRACK / POP (unmark1). Next candidate:3. - Step 17 (Branch Right: Choose 3, d=2):
path = [2, 3],used = {2, 3}. Loop candidates[1, 2, 3](2, 3in used skip). - Step 18 (Choose 1, d=3):
path = [2, 3, 1],used = {1, 2, 3}. Base Case (len == 3): Record[2, 3, 1]✅ (#4). - Step 19 (Backtrack / Pop 1, d=2):
path = [2, 3],used = {2, 3}↩️ BACKTRACK / POP (unmark1). - Step 20 (Backtrack / Pop 3, d=1):
path = [2],used = {2}↩️ BACKTRACK / POP (unmark3). - Step 21 (Backtrack / Pop 2, d=0):
path = [],used = {}↩️ BACKTRACK / POP to Root (unmark2). Next candidate:3. - Step 22 (Choose 3, d=1):
path = [3],used = {3}. Loop candidates[1, 2, 3](3in used skip). - Step 23 (Branch Left: Choose 1, d=2):
path = [3, 1],used = {1, 3}. Loop candidates[1, 2, 3](1, 3in used skip). - Step 24 (Choose 2, d=3):
path = [3, 1, 2],used = {1, 2, 3}. Base Case (len == 3): Record[3, 1, 2]✅ (#5). - Step 25 (Backtrack / Pop 2, d=2):
path = [3, 1],used = {1, 3}↩️ BACKTRACK / POP (unmark2). - Step 26 (Backtrack / Pop 1, d=1):
path = [3],used = {3}↩️ BACKTRACK / POP (unmark1). Next candidate:2. - Step 27 (Branch Right: Choose 2, d=2):
path = [3, 2],used = {2, 3}. Loop candidates[1, 2, 3](2, 3in used skip). - Step 28 (Choose 1, d=3):
path = [3, 2, 1],used = {1, 2, 3}. Base Case (len == 3): Record[3, 2, 1]✅ (#6). - Step 29 (Backtrack / Pop 1, d=2):
path = [3, 2],used = {2, 3}↩️ BACKTRACK / POP (unmark1). - Step 30 (Backtrack / Pop 2, d=1):
path = [3],used = {3}↩️ BACKTRACK / POP (unmark2). - Step 31 (Backtrack / Pop 3, d=0):
path = [],used = {}↩️ BACKTRACK / POP to Root (unmark3). All 6 permutations generated! Search Complete: Output:[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]].
nums = [1,2,3]Expected:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]| 1 | results, path, used = [], [], [False] * len(nums) |
| 2 | def backtrack(): |
| 3 | if len(path) == len(nums): |
| 4 | results.append(list(path)) |
| 5 | return |
| 6 | for i in range(len(nums)): |
| 7 | if used[i]: |
| 8 | continue |
| 9 | used[i] = True |
| 10 | path.append(nums[i]) |
| 11 | backtrack() |
| 12 | path.pop() |
| 13 | used[i] = False |
| 14 | backtrack() |
| 15 | return results |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Every backtracking archetype so far used a start index to avoid reusing earlier elements — but [1,2] and [2,1] are different permutations, so "only look forward from where I left off" is actively wrong here.
📚 The Analogy: Arranging Books on a Shelf
Imagine arranging three books on a shelf, one slot at a time. For the first slot, you can pick any of the three books — there's no "start" restricting you to some subset. Once a book is placed, it's physically on the shelf and can't be picked again for a later slot — so you keep a mental note of "already placed" books, and skip over them each time you scan your full collection for the next slot.
🔑 The Key Idea in the Code
Two pieces of state get chosen and un-chosen together (path and used[i]) — forgetting to revert either one leaves the recursion in a corrupted state for every sibling branch that runs afterward.
Using linear scan
if num in path( lookup): Checking membership in a list takes time per step. Use a booleanusedarray or in-place swappingnums[start], nums[i] = nums[i], nums[start]to achieve checks.Forgetting to undo in-place swap: When using the swap method, failing to swap back
nums[start], nums[i] = nums[i], nums[start]after recursion corrupts array order for sibling branches.Appending without shallow copy: Appending
pathinstead ofpath[:]stores references that get cleared upon backtrack.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
The statement asks for "all the possible permutations" of an array of "distinct integers", with at most 6 of them: order matters and every element is used exactly once, so drop start, loop from 0 every time, and skip a position whose used[i] is true.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: path holds the elements chosen so far, in order, and used[i] is true exactly when nums[i] is in path. Position i is choosable only while used[i] is false. Complete when len(path) == len(nums).
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Appending live path reference to results:
results.append(path)appends a mutable reference; when backtracking pops elements, the recorded result becomes empty. Always append a copy:results.append(list(path))orpath[:].Using a
startindex instead ofused[i]-- permutations can place an earlier-in-the-list element after a later one (e.g. choosing 1 after 2), which a forward-only start index forbids.Undoing only
path(pop) but notused[i] = False-- the code still looks symmetric, but the mark leaks into sibling branches.Off-by-one on the completion check --
len(path) == len(nums), notlen(path) == len(nums) - 1.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use Permutations. Any position of the ordering may hold any element that is not already in path, so the loop always starts at index 0 and skips i when used[i] is true. For each other i, I set used[i] to True, append nums[i], call backtrack, then pop and set used[i] back to False. Because every choice is undone after its branch, each full path is a different sequence of choices, and with distinct values a different ordering, so I save every ordering exactly once when path is as long as nums. The trap is the undo: if I forget used[i] = False after the call, every later branch still treats that element as taken, and for [1, 2, 3] I get only [[1, 2, 3]]. There are N! orderings, each copied in
O(N), so it's O(N times N!) time, andO(N)extra space for path, used and the recursion, not counting the output."
Complexity & Mathematical Proof
O(N! · N)
Look at the recursive decision tree: At depth 0, we choose from N elements. At depth 1, we choose from N-1 elements. At depth k, we choose from N-k elements. The decision tree branches into exactly N · (N-1) · (N-2) · ... · 1 = N! leaf states. At each of the N! leaf states, we make a deep copy list(path) of length N to append to results, which takes O(N) time per leaf. Multiplying N! leaves by O(N) copy cost gives total O(N! · N) time.
O(N) Call Stack
Look at memory allocation: The recursive call stack descends to maximum depth N before hitting a base case and returning (O(N) stack frames). The mutable list path holds at most N elements, and the boolean lookup array used uses N entries. Excluding the output list results (which holds N! permutations of size N), auxiliary memory is strictly O(N).
T(N) = N! · O(N)
Derivation Progression
N! leaf permutation states
Branching factor starts at N and decrements at each depth, creating N! total permutation leaves.
O(N) list(path) deep copy
When path reaches length N, copying path into results takes O(N) time.
O(N! · N)
N! leaves multiplied by O(N) snapshot work per leaf yields strictly O(N! · N) time.
Variable Definitions
Number of elements in the input array
Memory Architecture & Bounds
O(N) Recursion call stack of maximum depth N
O(N) path list of length N and boolean used array of size N
O(N! · N) Result list storing all N! permutations
Boundary Best / Worst Cases
Decision & State-Space Search Tree
Senior SWE Deconstruction & Hardware Caveats
Triggers: "all possible permutations", "distinct integers". State-tracking via boolean used array: every position in the permutation can select any unused number from the input.
leaves.
Using start index: permutations must inspect elements before i as well, so do NOT use start.
Permutations (LeetCode 46)
Given an array nums of distinct integers, return every possible ordering of all its elements -- i.e., every permutation -- in any order.
A permutation uses each element from nums exactly once, arranged into some linear sequence.
Worked Examples
nums = [1,2,3][[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]3 * 2 * 1 = 6 ways.nums = [0,1][[0,1],[1,0]]nums = [1][[1]]⚖️Formal Constraints & Bounds
1 <= nums.length <= 6-10 <= nums[i] <= 10All the integers of
numsare unique.
Core Insight
A permutation uses every element, only reordering them -- unlike a subset, which may leave elements out. Because an earlier element may still be needed later, a used set (not a start index) tracks which elements are currently placed; both path and used must be reverted together on backtrack.
Real-World Scenario & Production Applications
Scheduling every possible ordering of a fixed set of tasks on a single machine, or enumerating every seating arrangement at a table, both require generating every permutation of a small fixed set.
Subproblems & Recurrence Decomposition3 Phases
Iterate through all elements in nums and branch on candidates that have not yet been used in the active permutation (not used[i]).
for i in range(len(nums)):
if not used[i]:Step-by-Step Execution Trace Table
[ ] (Root, used={}, d=0) / | \ choose 1 / choose 2 | choose 3 \ / | \ [ 1 ] (d=1) [ 2 ] (d=1) [ 3 ] (d=1) / \ / \ / \ choose 2/ choose 3 \ choose 1/ choose 3 \ choose 1/ choose 2 \ / \ / \ / \ [1,2] (d=2) [1,3] (d=2)[2,1] (d=2) [2,3] (d=2)[3,1] (d=2) [3,2] (d=2) | | | | | | choose 3| choose 2| choose 3| choose 1| choose 2| choose 1| v v v v v v [1,2,3] ✅ [1,3,2] ✅[2,1,3] ✅ [2,3,1] ✅[3,1,2] ✅ [3,2,1] ✅ Step-by-Step Decision Trace (for nums = [1, 2, 3])
- Step 1 (Root, d=0):
path = [],used = {}. Loop available candidates[1, 2, 3]. - Step 2 (Choose 1, d=1):
path = [1],used = {1}. Loop candidates[1, 2, 3](1in used skip). - Step 3 (Branch Left: Choose 2, d=2):
path = [1, 2],used = {1, 2}. Loop candidates[1, 2, 3](1, 2in used skip). - Step 4 (Choose 3, d=3):
path = [1, 2, 3],used = {1, 2, 3}. Base Case (len == 3): Record[1, 2, 3]✅ (#1). - Step 5 (Backtrack / Pop 3, d=2):
path = [1, 2],used = {1, 2}↩️ BACKTRACK / POP (unmark3). - Step 6 (Backtrack / Pop 2, d=1):
path = [1],used = {1}↩️ BACKTRACK / POP (unmark2). Next candidate:3. - Step 7 (Branch Right: Choose 3, d=2):
path = [1, 3],used = {1, 3}. Loop candidates[1, 2, 3](1, 3in used skip). - Step 8 (Choose 2, d=3):
path = [1, 3, 2],used = {1, 2, 3}. Base Case (len == 3): Record[1, 3, 2]✅ (#2). - Step 9 (Backtrack / Pop 2, d=2):
path = [1, 3],used = {1, 3}↩️ BACKTRACK / POP (unmark2). - Step 10 (Backtrack / Pop 3, d=1):
path = [1],used = {1}↩️ BACKTRACK / POP (unmark3). - Step 11 (Backtrack / Pop 1, d=0):
path = [],used = {}↩️ BACKTRACK / POP to Root (unmark1). Next candidate:2. - Step 12 (Choose 2, d=1):
path = [2],used = {2}. Loop candidates[1, 2, 3](2in used skip). - Step 13 (Branch Left: Choose 1, d=2):
path = [2, 1],used = {1, 2}. Loop candidates[1, 2, 3](1, 2in used skip). - Step 14 (Choose 3, d=3):
path = [2, 1, 3],used = {1, 2, 3}. Base Case (len == 3): Record[2, 1, 3]✅ (#3). - Step 15 (Backtrack / Pop 3, d=2):
path = [2, 1],used = {1, 2}↩️ BACKTRACK / POP (unmark3). - Step 16 (Backtrack / Pop 1, d=1):
path = [2],used = {2}↩️ BACKTRACK / POP (unmark1). Next candidate:3. - Step 17 (Branch Right: Choose 3, d=2):
path = [2, 3],used = {2, 3}. Loop candidates[1, 2, 3](2, 3in used skip). - Step 18 (Choose 1, d=3):
path = [2, 3, 1],used = {1, 2, 3}. Base Case (len == 3): Record[2, 3, 1]✅ (#4). - Step 19 (Backtrack / Pop 1, d=2):
path = [2, 3],used = {2, 3}↩️ BACKTRACK / POP (unmark1). - Step 20 (Backtrack / Pop 3, d=1):
path = [2],used = {2}↩️ BACKTRACK / POP (unmark3). - Step 21 (Backtrack / Pop 2, d=0):
path = [],used = {}↩️ BACKTRACK / POP to Root (unmark2). Next candidate:3. - Step 22 (Choose 3, d=1):
path = [3],used = {3}. Loop candidates[1, 2, 3](3in used skip). - Step 23 (Branch Left: Choose 1, d=2):
path = [3, 1],used = {1, 3}. Loop candidates[1, 2, 3](1, 3in used skip). - Step 24 (Choose 2, d=3):
path = [3, 1, 2],used = {1, 2, 3}. Base Case (len == 3): Record[3, 1, 2]✅ (#5). - Step 25 (Backtrack / Pop 2, d=2):
path = [3, 1],used = {1, 3}↩️ BACKTRACK / POP (unmark2). - Step 26 (Backtrack / Pop 1, d=1):
path = [3],used = {3}↩️ BACKTRACK / POP (unmark1). Next candidate:2. - Step 27 (Branch Right: Choose 2, d=2):
path = [3, 2],used = {2, 3}. Loop candidates[1, 2, 3](2, 3in used skip). - Step 28 (Choose 1, d=3):
path = [3, 2, 1],used = {1, 2, 3}. Base Case (len == 3): Record[3, 2, 1]✅ (#6). - Step 29 (Backtrack / Pop 1, d=2):
path = [3, 2],used = {2, 3}↩️ BACKTRACK / POP (unmark1). - Step 30 (Backtrack / Pop 2, d=1):
path = [3],used = {3}↩️ BACKTRACK / POP (unmark2). - Step 31 (Backtrack / Pop 3, d=0):
path = [],used = {}↩️ BACKTRACK / POP to Root (unmark3). All 6 permutations generated! Search Complete: Output:[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]].
nums = [1,2,3]Expected:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]| 1 | results, path, used = [], [], [False] * len(nums) |
| 2 | def backtrack(): |
| 3 | if len(path) == len(nums): |
| 4 | results.append(list(path)) |
| 5 | return |
| 6 | for i in range(len(nums)): |
| 7 | if used[i]: |
| 8 | continue |
| 9 | used[i] = True |
| 10 | path.append(nums[i]) |
| 11 | backtrack() |
| 12 | path.pop() |
| 13 | used[i] = False |
| 14 | backtrack() |
| 15 | return results |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Every backtracking archetype so far used a start index to avoid reusing earlier elements — but [1,2] and [2,1] are different permutations, so "only look forward from where I left off" is actively wrong here.
📚 The Analogy: Arranging Books on a Shelf
Imagine arranging three books on a shelf, one slot at a time. For the first slot, you can pick any of the three books — there's no "start" restricting you to some subset. Once a book is placed, it's physically on the shelf and can't be picked again for a later slot — so you keep a mental note of "already placed" books, and skip over them each time you scan your full collection for the next slot.
🔑 The Key Idea in the Code
Two pieces of state get chosen and un-chosen together (path and used[i]) — forgetting to revert either one leaves the recursion in a corrupted state for every sibling branch that runs afterward.
Using linear scan
if num in path( lookup): Checking membership in a list takes time per step. Use a booleanusedarray or in-place swappingnums[start], nums[i] = nums[i], nums[start]to achieve checks.Forgetting to undo in-place swap: When using the swap method, failing to swap back
nums[start], nums[i] = nums[i], nums[start]after recursion corrupts array order for sibling branches.Appending without shallow copy: Appending
pathinstead ofpath[:]stores references that get cleared upon backtrack.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
The statement asks for "all the possible permutations" of an array of "distinct integers", with at most 6 of them: order matters and every element is used exactly once, so drop start, loop from 0 every time, and skip a position whose used[i] is true.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: path holds the elements chosen so far, in order, and used[i] is true exactly when nums[i] is in path. Position i is choosable only while used[i] is false. Complete when len(path) == len(nums).
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Appending live path reference to results:
results.append(path)appends a mutable reference; when backtracking pops elements, the recorded result becomes empty. Always append a copy:results.append(list(path))orpath[:].Using a
startindex instead ofused[i]-- permutations can place an earlier-in-the-list element after a later one (e.g. choosing 1 after 2), which a forward-only start index forbids.Undoing only
path(pop) but notused[i] = False-- the code still looks symmetric, but the mark leaks into sibling branches.Off-by-one on the completion check --
len(path) == len(nums), notlen(path) == len(nums) - 1.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use Permutations. Any position of the ordering may hold any element that is not already in path, so the loop always starts at index 0 and skips i when used[i] is true. For each other i, I set used[i] to True, append nums[i], call backtrack, then pop and set used[i] back to False. Because every choice is undone after its branch, each full path is a different sequence of choices, and with distinct values a different ordering, so I save every ordering exactly once when path is as long as nums. The trap is the undo: if I forget used[i] = False after the call, every later branch still treats that element as taken, and for [1, 2, 3] I get only [[1, 2, 3]]. There are N! orderings, each copied in
O(N), so it's O(N times N!) time, andO(N)extra space for path, used and the recursion, not counting the output."
Complexity & Mathematical Proof
O(N! · N)
Look at the recursive decision tree: At depth 0, we choose from N elements. At depth 1, we choose from N-1 elements. At depth k, we choose from N-k elements. The decision tree branches into exactly N · (N-1) · (N-2) · ... · 1 = N! leaf states. At each of the N! leaf states, we make a deep copy list(path) of length N to append to results, which takes O(N) time per leaf. Multiplying N! leaves by O(N) copy cost gives total O(N! · N) time.
O(N) Call Stack
Look at memory allocation: The recursive call stack descends to maximum depth N before hitting a base case and returning (O(N) stack frames). The mutable list path holds at most N elements, and the boolean lookup array used uses N entries. Excluding the output list results (which holds N! permutations of size N), auxiliary memory is strictly O(N).
T(N) = N! · O(N)
Derivation Progression
N! leaf permutation states
Branching factor starts at N and decrements at each depth, creating N! total permutation leaves.
O(N) list(path) deep copy
When path reaches length N, copying path into results takes O(N) time.
O(N! · N)
N! leaves multiplied by O(N) snapshot work per leaf yields strictly O(N! · N) time.
Variable Definitions
Number of elements in the input array
Memory Architecture & Bounds
O(N) Recursion call stack of maximum depth N
O(N) path list of length N and boolean used array of size N
O(N! · N) Result list storing all N! permutations
Boundary Best / Worst Cases
Decision & State-Space Search Tree
Senior SWE Deconstruction & Hardware Caveats
Triggers: "all possible permutations", "distinct integers". State-tracking via boolean used array: every position in the permutation can select any unused number from the input.
leaves.
Using start index: permutations must inspect elements before i as well, so do NOT use start.