Problem overview
The method permute receives a list nums of distinct integers and returns every permutation of it: every way to put all the numbers in a row, each exactly once.
The order of the permutations is fixed. The method fills the positions from first to last and always tries the numbers in their order in nums. Every permutation starting with nums[0] comes first, then every one starting with nums[1], and so on. Among permutations with the same first number, the second number decides the same way. So nums itself is always first and nums reversed is always last.
For nums = [1, 2, 3] the result is:
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
Two permutations start with 1, two with 2 and two with 3. For nums = [3, 1, 2] the numbers are tried in the order 3, 1, 2, not in ascending order, so the result is [[3, 1, 2], [3, 2, 1], [1, 3, 2], [1, 2, 3], [2, 3, 1], [2, 1, 3]]. For nums = [1] it is [[1]].
Approach: fill one position at a time
Think of a row of empty slots, with one slot per number. The first slot can take any of the n numbers, the second can take any of the n - 1 that are left, and so on. The count is n * (n - 1) * ... * 1, which is n!. For 8 numbers that is 40,320 permutations.
Backtracking builds them one slot at a time:
- Keep a list
currentwith the numbers placed so far, and a listusedof True/False values, one per number, that says which numberscurrentalready holds. - When
currenthas as many numbers asnums, it is a whole permutation, so save a copy. - Otherwise go through the numbers in order. Skip the used ones. For each free one, take it (mark it used and add it to
current), fill the rest of the slots with a recursive call, then give it back (remove it and mark it free), so the next number can take its place.
The loop always starts at index 0 and moves up, which is what yields the order the statement describes. Giving the number back is the "back" in backtracking: when the recursive call returns, current and used are exactly as they were before it, so the loop can try the next candidate for the same slot.
Step by step
- Create
resultas an empty list of permutations. - Create
usedas[False] * len(nums).used[i]says whethernums[i]is already in the permutation being built. At the start, every number is free. - Start the search with
self.backtrack(nums, used, [], result): no slot is filled yet. The helper comes next. - Return
result. The helper adds to this same list. - Declare the helper
backtrack(self, nums, used, current, result). Each call fills one more slot ofcurrentand calls itself for the rest. - Test whether
currentis full:len(current) == len(nums). A full list holds every number once. - Save a copy and return.
current.copy()is a snapshot, becausecurrentkeeps changing. - Loop with
ioverrange(len(nums)): every number that could fill the next slot, in the order ofnums. - Skip used numbers with
if used[i]: continue. A number can't appear twice in a permutation. - Mark the number by setting
used[i] = True, so deeper calls won't take it. - Add it to
currentwithappend. This fills the slot. - Recurse with
self.backtrack(nums, used, current, result). Nothing else needs passing, becauseusedandcurrentalready describe what is taken. - Remove the number from the end of
currentwithpop(). - Unmark it by setting
used[i] = False, so later permutations can use it in another position.
The solution in Python
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
result = []
used = [False] * len(nums)
self.backtrack(nums, used, [], result)
return result
def backtrack(self, nums, used, current, result):
if len(current) == len(nums):
result.append(current.copy())
return
for i in range(len(nums)):
if used[i]:
continue
used[i] = True
current.append(nums[i])
self.backtrack(nums, used, current, result)
current.pop()
used[i] = False
Python details worth a closer look:
- Repeating a list.
[False] * 3is[False, False, False]. Multiplying a list of immutable values like booleans is safe. Multiplying a list of lists would repeat references to the same inner list, which is a different trap. continue. It skips the rest of the current turn of the loop and goes on with the nexti, so the code below it only runs for free numbers.- Shared lists.
usedandcurrentare passed by reference: every call sees and changes the same two lists. That makes the marking and the undo work, and it is why the saved permutation must be a copy ofcurrent. - The undo must be complete. Taking a number changes two things,
usedandcurrent, and giving it back must undo both. If the lineused[i] = Falseis missing, the first permutation is found and nothing else: for[1, 2, 3]the result is just[[1, 2, 3]], because the numbers stay marked as taken. - Indexes, not values.
usedis indexed by position innums, not by number. That keeps it correct for negative numbers and for any values, since the lesson only needs the numbers to be distinct.
Complexity
Time: O(n * n!). There are n! permutations, and copying one takes n steps. The calls that build them, fewer than e * n! in all, each loop over the n numbers, which keeps the total at O(n * n!).
Space: O(n) besides the answer. The extra memory is current, used and one call on the stack per slot. The answer itself holds n! lists and is output, so it isn't counted.
Edge cases and common mistakes
- One number.
[1]gives[[1]]: a single slot has a single way to be filled. - The order of tries.
[3, 1, 2]starts with 3, because the loop follows the order ofnums. Sortingnumsfirst would change the answer's order and fail the lesson's tests. - Forgetting the copy. Appending
currentitself stores one list many times, and at the end that list is empty. - Undoing only one of the two changes. Leaving a number marked as used loses permutations, as shown above. Leaving a number in
currentmakescurrenttoo long, and the lists that get saved are wrong.
Testing
nums = [1, 2, 3]gives[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]].nums = [3, 1, 2]gives[[3, 1, 2], [3, 2, 1], [1, 3, 2], [1, 2, 3], [2, 3, 1], [2, 1, 3]].nums = [1]gives[[1]].nums = [0, 1, 2, 3, 4, 5, 6, 7]gives a list of 40320 permutations.
