2

I am trying to understand this recursion. I know how recursion works in factorial function but when it gets to this complex recursion like this I am confused. The most confusing part to me is this code

str.split('').map( (char, i) => 
    permutations( str.substr(0, i) + str.substr(i + 1) )map( p => char + p))

First, with "abc", say, it will split into ["a","b","c"] and go through the map function, then go through the second map function to wrap each return with a, b, c, respectively. However, I am very confused at the recursion part.

I thought the first recursion in "a" with value of str as "abc" will return "bc", and second recursion with str value of "bc" will return "c", and so on.

But when I just ran this code to see a clear recursion, it returns

[ [ [ 'c' ], [ 'b' ] ], [ [ 'c' ], [ 'a' ] ], [ [ 'b' ], [ 'a' ] ] ]

This is most confusing to me. I just can't see how this recursion returns these values. Can anyone go more in detail through how this work, like illustrating your thought process step by step?

I am a visual learner. Thank you for your help.

function permutations(str) {
 return (str.length <= 1) ? [str] :
      // Array.from(new Set(
        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')
Will Ness
  • 70,110
  • 9
  • 98
  • 181
Ckate
  • 47
  • 6
  • As to your edit, are you sure that's what you get? When I run the code, I get `[[["abc", "acb"], ["bac", "bca"], ["cab", "cba"]]`. To make this work anything like appropriately, though, you will need to uncomment the `reduce` call. When I do so, I get the correct `["abc", "acb", "bac", "bca", "cab", "cba"]`. – Scott Sauyet Jan 08 '21 at 17:32
  • 1
    The output you display looks as though it also commented out the inner `.map` call. If you are really asking how someone else's code works, you really need to keep that code. If you want to know why your own attempts do not work, please ask that question instead. – Scott Sauyet Jan 08 '21 at 17:36

3 Answers3

3

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.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • in your concluding example of proof by induction, you've only used the assumption it's true for `n-1`, not for *all under `n`* (and it was enough). and indeed the ordinary principle of induction states just that, for the `n-1 => n` induction step (with *all* under `n` it's called ["complete (or strong) induction"](https://en.wikipedia.org/wiki/Mathematical_induction#Complete_(strong)_induction)). – Will Ness Jan 08 '21 at 14:59
  • @WillNess: Well, maybe I didn't make it clear enough that because it's true for all integers below `n` (which is the assumption I stated) then it's true for `n - 1`. Granted that's all I needed to use for the proof, but I intentionally used strong induction as that is often what's necessary for the analogy to recursion. And of course strong and weak induction are entirely equivalent in what they can show, which is what I tried to state in the last paragraph of that footnote. – Scott Sauyet Jan 08 '21 at 15:08
  • @WillNess: Added a parenthetical remark, so the sentence now reads "But we know that the statement is true for `n - 1` (since we assumed it's true for all integers below `n`)," Does that help? – Scott Sauyet Jan 08 '21 at 15:15
  • if it's intentional, I shouldn't butt in. :) it's just that sometimes we need to make several recursive calls, sometimes just one is enough. the latter is a subcase of the former, yes, but the distinction is still useful. (I've just googled "does histomorphism correspond to strong induction?" and it gave me some PDF with the quote "The Mendler style histomorphism (which corresponds to strong induction) ..." so the answer seems to be yes. the simple recursion is a catamorphism AFAICT. which is simpler, and one that can be transformed into a loop, I imagine...) – Will Ness Jan 08 '21 at 15:53
  • @WillNess: Butting in is what SO is all about! Now I have to go look up histomorphisms (I was in a math PhD program many years ago [never finished, and escaped with a MA] and Category Theory was by far my least favorite course, so thanks a lot for the homework!) While the strong/weak induction distinction is sometimes useful, we need go only as far as Fibonacci to need the stronger form, so I always use the stronger form to make this analogy. And it is only an analogy. I think it's a good one, but the main pedagogical use is simply in "assume it works for simpler cases, then..." – Scott Sauyet Jan 08 '21 at 16:21
  • 1
    all I know about it is its name, and that apparently it is recursion which has access to the whole *history* of it(self?). that's why I made that assumption, in that google query. as for the "simpler cases", "simpler case" is even simpler! :) (and I always prefer the simplest way to solve a problem, if I can find one.) – Will Ness Jan 08 '21 at 16:53
2

Let's examine permutations('abc').

'abc' is converted to ['a','b','c'] for mapping

Map

a

First, char='a',i=0. Note that permutations(str.substr(0, i) + str.substr(i + 1)) means "get the permutations of all the characters EXCEPT the one I'm looking at. In this case, this means permutations('bc'). Let's assume this gives the correct outputs ['bc','cb'], as the inductive hypothesis.

.map(p => char + p) then tells us to prepend the character we are looking at ('a') to each of the smaller permutations. This yields ['abc',acb'].

b

Following the same logic, char='b',i=1'. permutations('ac') == ['ac','ca']. Final outputs are ['bac','bca']

c

Following the same logic, char='c',i=2'. permutations('ab') == ['ab','ba']. Final outputs are ['cab','cba'].

Thus the overall output of the function would be [['abc','acb'],['bac','bca'],['cab','cba']]...

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Ted Brownlow
  • 1,103
  • 9
  • 15
  • 2
    Note that the (commented out) `reduce` call is necessary. If you try this with `'abcd'`, the results will be ... odd. My answer shows how to replace it with `flatMap`, but you need this step to make it work properly. – Scott Sauyet Jan 06 '21 at 19:28
  • Thanks, but how did permutations(str.substr(0, i) + str.substr(i + 1)) return ['bc','cb']. It stills does't make sense to me. – Ckate Jan 06 '21 at 20:18
  • with the 'bc' case when we look at 'b', then all other characters are just ['c']. then we prepend 'b' to to get 'bc'. Similarly, when we look at 'c', then all other characters are just ['b']. then we prepend 'c' to get 'cb'. Thus, the overall output is ['bc','cb'] – Ted Brownlow Jan 06 '21 at 21:07
  • 1
    as @ScottSauyet says, what you show is impossible. you assume as induction hypothesis that permutations of a string is a list of strings, then show an expansion step that returns ... the list of *lists* of strings. so the types don't fit -- the list must be flattened :) either by flatMap or reduce. (an interesting side note is this parallel b/w monadic join and fold/reduce, summing a list's values (here, lists) into one value (list)... (wondering whether "monad" can be said to be "higher-order monoid" in some way... probably yes)) – Will Ness Jan 08 '21 at 17:27
  • but thanks a lot for the step by step. it helped to understand the code (as I don't know JS myself). – Will Ness Jan 08 '21 at 17:29
  • 1
    @CKate: You can see my answer for more details. But the basic point is that you can assume the function works for the simpler cases and show that means it works for the current case. Together with the demonstration that it works for the simplest cases, and that each recursive call is to a simpler case, you've just shown that it works for all cases. So we can simply *assume* that `permutations('bc')` yields `['bc', 'cb']`. You can always choose to walk through that example the same way. – Scott Sauyet Jan 08 '21 at 17:40
0

This is actually a pretty unusual definition of permutations, one that I happen to never have seen before.

(well, as it turns out, I actually wrote an answer once with its near exact equivalent, and only a few years prior... oh my).

In pseudocode, the simply-recursive definition one usually sees is

perms [x, ...xs] = [ [...as, x, ...bs] | p <- perms xs, (as, bs) <- splits p]

but this one is

perms2 xs = [ [x, ...p] | (as, [x, ...bs]) <- splits xs, p <- perms2 [...as, ...bs]]

(with list comprehensions and patterns; sans the empty list cases; with the "natural" definition of splits which builds a list of all possibilities of breaking a list up into two parts).

There's a certain duality here... Interesting. And not "simply"-recursive. :)

Or, with some more named functions to be implemented in obvious ways,

perms [x, ...rest] = [ i | p <- perms rest, i <- inserts x p]
                   = flatMap (inserts x) (perms rest)

--- and this version, 
perms2 xs = [ [x, ...p] | (x, rest) <- picks xs, p <- perms2 rest]

See also:

Will Ness
  • 70,110
  • 9
  • 98
  • 181