One way I prefer to analyze and create recursive solutions is to work as though it's mathematical induction1.
The trick is to show that the function returns the right value for our base case(s), and then show that if it returns the right value for our simpler cases, it will also return the right value for our current case. Then we know that it will work for all values, as long as each recursive call is to some simpler case that eventually leads to a base case.
So look at your function. I've reformatted it to make discussion easier, and I've restored the reduce
call you've commented out. That turns out to be necessary to do this right (although we'll discuss a more modern alternative below.) You also commented out the Array .from (new Set( ... ))
wrapper, which is used to remove duplicates in the case your string has repeated characters. Without this, "aba"
returns ["aba", "aab", "baa", "baa", "aab", "aba"]
. With it, we get ["aba", "aab", "baa"]
, which makes more sense. But that is separate from our recursion question.
The cleaned-up function looks like this:
function permutations (str) {
return (str .length <= 1)
? [str]
: str
.split ('')
.map ((char, i) =>
permutations (str .substr (0, i) + str.substr (i + 1))
.map (p => char + p)
)
.reduce ((r, x) => r .concat (x), [])
}
permutations('abc')
Our base cases are pretty simple, str.length <= 1
. In that case we yield [str]
. This only has two possibilities: the string is empty, and we return ['']
, or the string has a single character, say 'x'
, and we return ['x']
. These are pretty clearly correct, so we move on to the recursive call.
Let's say we pass 'abc'
. The split
and map
calls turn that into the equivalent of this:
[
permutations ('bc') .map (p => 'a' + p),
permutations ('ac') .map (p => 'b' + p),
permutations ('ab') .map (p => 'c' + p),
]
But we have made the assumption that our recursion works on the smaller strings of 'bc'
, 'ac'
, and 'ab'
. That means that permutations('bc')
will yield ['bc', 'cb']
, and similarly for the others, so this is equivalent to
[
['bc', 'cb'] .map (p => 'a' + p),
['ac', 'ca'] .map (p => 'b' + p),
['ab', 'ba'] .map (p => 'c' + p),
]
which is
[
['abc', 'acb']
['bac', 'bca']
['cab', 'cba']
]
Now we do the reduce
call, which successively concatenates each array onto the previous result, starting with []
, to get
['abc', 'acb', 'bac', 'bca', 'cab', 'cba']
There is a cleaner way of doing this. We can replace the map
call followed by this reduce
call with a single flatMap
call, like this:
function permutations (str) {
return (str .length <= 1)
? [str]
: str
.split ('')
.flatMap ((char, i) =>
permutations (str .substr (0, i) + str.substr (i + 1))
.map (p => char + p)
)
}
In any case, we've demonstrated our inductive trick. By assuming this works for the simpler cases, we show that it works for out current case. (And no, we haven't done this rigorously, only by example, but it wouldn't be terribly difficult to prove this with some sort of mathematical rigor.) When we combine that with the demonstration that it works for the base case, we show that it works for all cases. This depends on our recursive calls being simpler in some way that eventually leads to a base case. Here, the strings being passed to the recursive call are one character shorter than those we were supplied, so we know that eventually we will hit our str .length <= 1
condition. And thus we know that it works.
If you add the Array .from (new Set ( ... ))
wrapper back on, this will also work for those cases with repeating characters.
1
You may or may not have run across induction, and you may or may not remember it if you did, but in essence, it's very simple. Here's a very simple mathematical induction argument:
We will prove that 1 + 2 + 3 + ... + n == n * (n + 1) / 2
, for all positive integers, n
.
First, we can easily see that it's true when n
is 1
: 1 = 1 * (1 + 1) / 2
Next we assume that the statement is true for all integers below n
.
We show that it's true for n
like this:
1 + 2 + 3 + ... + n
is the same as 1 + 2 + 3 + ... + (n - 1) + n
, which is (1 + 2 + 3 + ... (n - 1)) + n
. But we know that the statement is true for n - 1
(since we assumed it's true for all integers below n
), so 1 + 2 + 3 + ... + (n - 1)
is, by substituting in n - 1
for n
in the expression above, equal to (n - 1) * ((n - 1) + 1) / 2
, which simplifies to (n - 1) * n / 2
. So now our larger expression ((1 + 2 + 3 + ... (n - 1)) + n
is the same as ((n - 1) * n / 2) + n
, which we can simplify to (n^2 - n) / 2 + n
and then to (n^2 - n + (2 * n)) / 2
and to (n^2 + n) / 2
. which factors into n * (n + 1) / 2
.
So, by assuming it's true for everything less than n
we show that it's true for n
as well. Together with the fact that it's true when n
is 1
, the principle of induction says that it's true for all positive integers n
.
You may have seen induction stated slightly differently: If (a) it's true for 1
and (b) being true for n - 1
implies that it's true for n
, then (c) it's true for all positive integers n
. (The difference here is that we don't need the assumption that it's true for all integers below n
, only for n - 1
.) It's easy to prove the equivalence of these two models. And the everything below
formulation usually makes for a more convenient analogy in recursive problems.