Permutations
In one line
A permutation is an ordering of all the elements; n distinct elements have n! orderings, and backtracking builds them position by position using a used array.
What it is
A permutation is an arrangement of elements where order matters: [1, 2, 3] and [3, 2, 1] are different. With n distinct elements there are n! permutations, because there are n choices for the first position, n - 1 for the second, and so on.
Unlike subsets, every position may take any element that has not been used yet, not just elements to the right. So the template loops over all indices and keeps a used array (or set) to know which elements are already in the path.
If the input has duplicates, like [1, 1, 2], swapping the two 1s gives the same permutation. Sorting and a careful skip rule produce each distinct permutation once: 3! / 2! = 3 of them here.
Why interviewers ask
Permutations test whether you understand the difference between ordered and unordered choices, can manage a used array correctly, and know the duplicate-skip rule. They also show up inside bigger problems: trying all orders of tasks, letters or cities on small inputs.
How it works
- 1State: path (the arrangement so far) and used[i] (is nums[i] already placed).
- 2Goal: when len(path) == n, record a copy.
- 3Choices: every index i with used[i] False. Mark it used, append nums[i], recurse, then pop and unmark.
- 4Each level fills one position, so the tree has depth n and n * (n - 1) * ... * 1 = n! leaves.
- 5For duplicates: sort, and skip nums[i] if it equals nums[i - 1] and nums[i - 1] is not currently used. This forces equal values to be placed in their original order, so each distinct arrangement appears once.
- 6Alternative: swap-based permutation, which swaps nums[start] with each nums[i] for i >= start, recurses on start + 1, and swaps back; it uses no extra used array.
Worked example · permutations([1, 2, 3])
- Position 0 tries 1: used = [T, F, F], path [1].
- Position 1 tries 2: path [1, 2]; position 2 can only take 3: record [1, 2, 3]. Undo to [1].
- Position 1 tries 3: path [1, 3]; position 2 takes 2: record [1, 3, 2]. Undo to [].
- Position 0 tries 2: gives [2, 1, 3] and [2, 3, 1].
- Position 0 tries 3: gives [3, 1, 2] and [3, 2, 1].
- Six results = 3!. Each leaf is at depth 3 and each copy costs O(3).
Picture it
Seating guests at a table one chair at a time. For the first chair any guest can sit; for the next chair, anyone not already seated. When everyone is seated you write down the plan, then stand the last guest up and try someone else in that chair.
Reference code
def permutations(nums):
nums = sorted(nums) # sorting groups equal values
result, path = [], []
used = [False] * len(nums)
def backtrack():
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
continue # duplicate copy used out of order
used[i] = True
path.append(nums[i])
backtrack()
path.pop()
used[i] = False
backtrack()
return result
print(permutations([1, 2, 3])) # 6 orderings
print(permutations([1, 1, 2])) # [[1, 1, 2], [1, 2, 1], [2, 1, 1]]This is the standard used-array template plus one extra check for duplicates. With distinct input the duplicate check never fires. With repeated values it sorts, then refuses to place a duplicate value while its identical left neighbour is still unused, so the copies of a value are always placed left to right and each distinct ordering appears once. For [1, 1, 2] it returns [1, 1, 2], [1, 2, 1], [2, 1, 1].
Key patterns
Used array
When: Standard permutations of distinct items
How: Loop over all indices, skip used ones, mark before recursion, unmark after.
Swap in place
When: You want to avoid extra arrays
How: For i from start to n - 1: swap(start, i), recurse(start + 1), swap back.
Unique permutations
When: Input contains duplicates
How: Sort and skip nums[i] if nums[i] == nums[i - 1] and not used[i - 1].
Next permutation
When: You need the next arrangement in lexicographic order, not all of them
How: Find the rightmost i with a[i] < a[i + 1], swap a[i] with the rightmost larger element to its right, then reverse the suffix after i. O(n).
Real-world use
Brute-force route planning
For a handful of stops, delivery and travelling salesman tools can try every visiting order exactly; 8 stops is only 40,320 orders, while 15 stops is over a trillion, which is why larger instances use heuristics or DP.
Test ordering
Tools like pytest-randomly shuffle test order to expose hidden dependencies between tests; exhaustively checking all orders of a few tests is the permutation version of the same idea.
Anagram and word games
Word game solvers generate letter arrangements of a short rack and check them against a dictionary, usually pruning with a trie so invalid prefixes are dropped early.
In a project
A warehouse picker must visit 7 shelf locations and return to the packing desk; you want the shortest walking order for each order batch.
- Precompute walking distances between every pair of the 8 points.
- Backtrack over the order of the 7 shelves with a used array, carrying the distance walked so far.
- Prune any partial route whose distance already exceeds the best complete route found.
- Return the best order; 7! = 5,040 routes is tiny, and pruning cuts it further.
Impact: Pickers get the truly shortest route for small batches, cutting walking distance noticeably compared with visiting shelves in the order they appear on the ticket. The team also knows the cut-off where exhaustive search stops being practical (about 11 to 12 stops).
Trade-offs
Use it when
- Order matters and you must try or list every arrangement
- n is small, about 10 or fewer (10! is about 3.6 million)
- You can prune partial orderings by a cost bound
Avoid it when
- n is above about 12: 13! is over 6 billion
- The cost only depends on the set of visited items and the last item: use bitmask DP (Held-Karp is O(n^2 * 2^n))
- You only need the next or k-th permutation: there are direct O(n) and O(n^2) methods
Cost: n! permutations, each copied in O(n), so O(n * n!) time. Extra space O(n) for the path, used array and recursion depth.
Common mistakes
- Reusing the subsets start-index loop, which only produces one ordering of each set.
- Forgetting to reset used[i] = False after the recursive call.
- Getting the duplicate rule wrong: using 'used[i - 1]' when the standard rule is 'not used[i - 1]', or forgetting to sort first.
- Underestimating n!: claiming backtracking is fine for n = 15.
- Building a new list at every level (path + [x]) without noticing it adds O(n) per call.
Interview questions
Quick revision
- n distinct elements -> n! permutations.
- Loop over all indices; skip used; mark, recurse, unmark.
- Record only when len(path) == n.
- Duplicates: sort + skip if nums[i] == nums[i - 1] and not used[i - 1].
- Swap method: swap(start, i), recurse(start + 1), swap back.
- Time O(n * n!), extra space O(n).
- 10! is about 3.6 million; 12! is about 479 million.
Check yourself
1. How many distinct permutations does [1, 1, 2] have?
2. What is the main difference between the permutations loop and the subsets loop?
3. What is the time complexity of generating all permutations of n distinct elements?
4. In the unique-permutations rule, when is nums[i] skipped (after sorting)?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.