Problem overview
The method permute(nums) receives an array of distinct numbers and returns every permutation of it: every way to put all the numbers in a row, using each number exactly once. The result is an array of arrays.
The order of the result is fixed. Fill the positions from first to last, always trying the numbers in their order in nums. So every permutation that starts with nums[0] comes first, then every one that starts with nums[1], and so on, with the same rule applied to the second position, the third, and the rest.
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 [1] the result is [[1]].
Approach: build one position at a time
A permutation of n numbers has n positions. The first can hold any of the n numbers, the second any of the n - 1 that are left, and so on, so there are n! permutations. Any solution has to produce them all.
Backtracking builds them with one recursive function. It keeps the permutation so far in current. To fill the next position, it goes through the numbers in order and skips those already in current. For each free number it does three things: take it, fill the remaining positions with a recursive call, and give it back so that the next number can take its place. When current holds all the numbers, it is a finished permutation and gets saved.
To know which numbers are taken, the code keeps an array used of booleans, with used[i] telling whether nums[i] is in current. Looking up a flag by index is faster than searching current for the number each time.
Trying the numbers in the order of nums at every position produces exactly the order the problem asks for. Here are the first events of the search for [1, 2, 3]:
take 1 -> [1]
take 2 -> [1,2]
take 3 -> [1,2,3]
save [1,2,3]
give back 3 -> [1,2]
give back 2 -> [1]
take 3 -> [1,3]
take 2 -> [1,3,2]
save [1,3,2]
give back 2 -> [1,3]
give back 3 -> [1]
Step by step
- Create
resultas an empty array. It collects the permutations. - Create
usedwithnew Array(nums.length).fill(false): onefalseper number.new Array(n)alone makesnempty slots, andfill(false)sets each one. - Start the search with
this.backtrack(nums, used, [], result). Nothing is placed yet, so the permutation so far is empty and every number is free. - Return
result. The helper added to this same array. - Declare the helper
backtrack(nums, used, current, result)inside the class. Each call fills one more position. - Check whether
currentis full:current.length === nums.length. A fullcurrentholds every number once. - Save a copy and return.
[...current]copies the numbers into a new array.currentkeeps changing as the search goes on, so saving it directly would store something that changes later. - Loop over every index
ifrom 0 tonums.length - 1. Always starting at 0 and going up is what gives the required order. - Skip taken numbers. If
used[i]is true,continuemoves to the nexti. - Mark the number: set
used[i]totrue, so deeper calls won't take it again. - Push
nums[i]ontocurrent. This fills the next position. - Recurse with the same four arguments.
usedandcurrentalready describe what is taken, so nothing else is passed. - Give the number back with
current.pop(), which removes the last number. - Unmark it: set
used[i]back tofalse, so later permutations can place it elsewhere.
The solution in JavaScript
class Solution {
permute(nums) {
const result = [];
const used = new Array(nums.length).fill(false);
this.backtrack(nums, used, [], result);
return result;
}
backtrack(nums, used, current, result) {
if (current.length === nums.length) {
result.push([...current]);
return;
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
used[i] = true;
current.push(nums[i]);
this.backtrack(nums, used, current, result);
current.pop();
used[i] = false;
}
}
}
The JavaScript details worth a closer look:
- Arrays are passed by reference.
used,currentandresultare the same arrays in every call. That is what lets the recursion share oneresult, and it is also why every change tocurrentandusedmust be undone before the call returns. - The copy matters. If the code pushed
currentitself,resultwould hold several references to one array, and by the end that array is empty again. Fornums = [1, 2], a version that forgets the copy returns[[],[]]instead of[[1,2],[2,1]]. - The spread
[...current]builds a new array holding the same numbers. It makes a shallow copy, which is enough because the elements are numbers. new Array(n).fill(false). Withoutfill, the array has empty slots that read asundefined, which is falsy, so it would even seem to work. Filling it makes the intent explicit and gives a real array of booleans.continuein aforloop. It jumps to the incrementi++and then the next test, so skipping a taken number never skips the undo steps of another one.===for the length check. Both sides are numbers, and===compares without converting types.
Complexity
Time: O(n · n!). There are n! permutations, and saving each one copies n numbers. The recursion builds them through fewer than e · n! calls in total, and each call loops over the n numbers, so the whole search stays within O(n · n!). With n up to 8 that is 40,320 permutations at most.
Space: O(n) beyond the answer. The extra memory is current, used and the recursion, which is n calls deep. The answer itself holds n · n! numbers.
Edge cases and common mistakes
- One number.
[1]returns[[1]]: the first call takes the only number, andcurrentis full at once. - Two numbers.
[1, 2]returns[[1,2],[2,1]]. - Order follows
nums. For[3, 1, 2]the first permutation is[3,1,2], the array itself. Sortingnumsfirst would change the order of the result. - Forgetting the copy. The result then holds the same array several times, all empty at the end.
- Forgetting to unmark. If
used[i]staystrueafter the recursive call, later branches think the number is taken and the result comes out incomplete. - Forgetting
pop().currentkeeps numbers that should have been removed, so it no longer matches the marks inusedand the saved permutations are wrong. - Starting the loop at
i + 1. That is the pattern for subsets, where order doesn't matter. Permutations need every unused number at every position, so the loop starts at 0.
Testing
[1, 2, 3]gives[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]].[3, 1, 2]gives[[3,1,2],[3,2,1],[1,3,2],[1,2,3],[2,3,1],[2,1,3]].[1]gives[[1]].[1, 2]gives[[1,2],[2,1]].[1, 2, 3, 4, 5, 6, 7, 8]returns 40,320 permutations.
