Problem overview
The method permute(nums) receives an array of distinct integers and returns every permutation of it: every way to put all of its numbers in a row, each number exactly once.
The order of the permutations is fixed. Fill the positions from first to last, always trying the numbers in their order in nums. All the permutations that start with nums[0] come first, then those that start with nums[1], and so on; among permutations with the same first number, the second number decides in the same way. For [1, 2, 3] the answer is:
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
The order follows nums, not ascending values. For [3, 1, 2] the answer starts with [3, 1, 2], then [3, 2, 1], then the permutations that start with 1, and ends with [2, 1, 3]. With one number, [1] gives [[1]].
Approach: fill one position at a time
An array of n distinct numbers has n! permutations (6 for three numbers, 40,320 for eight), so the output itself is large and the task is to generate each permutation exactly once.
Backtracking builds a permutation one position at a time in a list current. For the next position, go through the numbers of nums in order and take each one that isn't already in current. Taking a number means marking it as used and appending it; a recursive call then fills the remaining positions. Afterwards the number is given back (removed and unmarked) so that the next number can take its place. When current holds all n numbers, a copy of it is saved.
The array used has one true/false value per number, and used[i] says whether nums[i] is already in current. It's how the search avoids using a number twice.
Because every loop goes through the numbers from index 0 upwards, the permutations come out in exactly the order described above.
Step by step
- Create
result, anumber[][]that starts empty and will collect the permutations. - Create
used, onefalseper number:new Array(nums.length).fill(false).new Array(n)makesnempty slots andfill(false)sets each of them. - Start the search with
this.backtrack(nums, used, [], result): no position is filled yet and every number is free. - Return
result. The helper filled this same array. - Declare the helper
backtrack(nums, used, current, result), returningvoid. Each call fills one more position ofcurrentand calls itself for the rest. - Check whether
currentis full:current.length === nums.lengthmeans it holds every number once. - Save a snapshot with
[...current], then return.currentkeeps changing, soresultneeds its own copy. - Loop over every index
ifrom 0 tonums.length - 1. Starting at 0 every time and going in order is what gives the order the answer needs. - Skip used numbers with
continue. A number already incurrentcan't appear twice. - Mark
nums[i]as used, so that deeper calls won't take it again. - Push it onto
current: it fills the next position. - Recurse to fill the remaining positions. Nothing else needs passing, since
usedandcurrentalready say what is taken. - Pop it from
currentto give the position back. - Unmark it,
used[i] = false, so permutations built later can use it somewhere else.
The solution in TypeScript
class Solution {
permute(nums: number[]): number[][] {
const result: number[][] = [];
const used: boolean[] = new Array(nums.length).fill(false);
this.backtrack(nums, used, [], result);
return result;
}
backtrack(nums: number[], used: boolean[], current: number[], result: number[][]): void {
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;
}
}
}
TypeScript details worth knowing
The copy matters. current is a single array shared by all the calls. Pushing it directly would store the same reference many times, and by the end of the search it is empty again, so every entry of result would show the same empty array. [...current] creates a new array holding the current numbers.
new Array(n).fill(false). new Array(3) alone is three empty slots, printed as [ <3 empty items> ], and fill(false) turns them into [false, false, false]. With fill and a number or boolean this is fine. With an object such as [], fill would put the same object in every slot: new Array(2).fill([]), followed by a push into the first element, shows [[1],[1]].
continue skips the rest of the current turn of the loop and moves on to the next i.
The types are checked by the compiler and erased when it produces JavaScript, so boolean[] and void don't exist at run time.
Complexity
Time: O(n · n!). There are n! permutations, and copying each one takes n steps. The calls that build them, fewer than e · n! in all, each loop over the n numbers, which gives the same O(n · n!) bound.
Space: O(n) besides the answer. The search holds current, used and one call per position, each of size at most n. The answer itself, with n! permutations, is not counted.
Edge cases and common mistakes
- One number.
[1]gives[[1]]. Two numbers,[1, 2], give[[1,2],[2,1]]. - Forgetting to unmark. If
used[i]stays true after the recursive call, later branches think the number is still taken and the result comes out short. - Forgetting
pop().currentgrows pastnnumbers and the "full" check never matches again. - Not copying
current. Every saved entry ends up pointing to the same array. - Looping from the wrong place. Starting the loop after the index just taken would miss permutations such as
[2, 1, 3]. Every position must try all the numbers from index 0, skipping the used ones. - Sorting
numsfirst. The order of the output followsnumsas given. For[3, 1, 2]the first permutation is[3, 1, 2], not[1, 2, 3].
Testing
The lesson's code gives these results:
permute([1, 2, 3]) -> [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
permute([3, 1, 2]) -> [[3,1,2],[3,2,1],[1,3,2],[1,2,3],[2,3,1],[2,1,3]]
permute([1]) -> [[1]]
permute([1, 2]) -> [[1,2],[2,1]]
For the numbers 1 to 8 it returns 40,320 permutations.
