1

I am recently learning about recursion due the factorial alogithm. Nevertheless, my question is:

  1. how do I simulate the "for loop" with a specified length
  2. how to push its indexes into an array with recursion?

for example, loop 5 times then push its "indexes" into an array rendering [1,2,3,4,5]

I have created 2 functions [ loop | recursive ] with each attempting to achieve the same output. The "for loop" version is as followed:

function loop(index, length) {
  const result = [];
  for (let i = index; i <= length; i++) result.push(i);
  return result;
}

console.log(loop(1, 5)); // [ 1, 2, 3, 4, 5 ]

However, the "recursive" version isn't printing the desire result.

function recursive(index, length) {
  const result = [];
  result.push(index);
  return (index < length) ? recursive(++index, length) : result;
}

console.log(recursive(1, 5)); // [ 5 ]

Can someone kindly show me why it didn't output [ 1, 2, 3, 4, 5 ]

aaandri98
  • 585
  • 4
  • 18
noirsociety
  • 314
  • 1
  • 2
  • 9

3 Answers3

4

Because result is being reinitialized to [] every time the function is called. You are not passing itself the previous result, so it gets reset. In the end, only the last iteration with the last index ([5]) is sent back.

What you can do is use a third argument, optional, that you can initialize to [].

function recursive(index,length, result=[]) {
  result.push(index);
  return (index < length) ? recursive(++index,length, result) : result;
}

console.log(recursive(1,5)); // [1,2,3,4,5]
Jeremy Thille
  • 26,047
  • 12
  • 43
  • 63
  • Yea its cleaner this way, thanks for your comment. – Nir Sep 08 '21 at 15:04
  • 1
    @Jeremy Thille --> Thank you for the prompt and mind blowing reply :) Not only did you provide the solution, your detail explanation is what make the learning process so encouraging and worthwhile. Btw, your implementation of "result" as the 3rd argument is simply brilliant – noirsociety Sep 08 '21 at 15:23
  • Ah, oh, well... hehe. Thanks :) – Jeremy Thille Sep 08 '21 at 16:44
  • Note, though that `recursive(5, 1)` should presumably return `[]`, but this returns `[5]`. An easy fix would be to guard the `push` with `if (index <= length)`. – Scott Sauyet Sep 10 '21 at 12:54
1

The answer from Jeremy Thille is great. It explains what's wrong, and gives a useful and understandable alternative. The code is clean and as it's tail-recursive, it can take advantage of tail-call optimization if that ever becomes widespread. There is a bug in it, though: presumably recursive (10, 3) should return an empty array. Here it will return [10].

While we could easily fix that, let's look instead at an alternative which is logically simpler and which avoids the default parameter. (An earlier answer explains some of the potential issues with default parameters. Please understand that they are still a useful and powerful technique, but they do have a downside.)

So here's an alternative version, both in modern syntax:

const range = (from, to) =>
  from > to ? [] : [from, ... range (from + 1, to)]

and in an older style:

function range (from, to) {
  if (from > to) {
    return []
  }
  return [from] .concat (range (from + 1, to))
}

In either version, we simply recur by creating a new array, starting with the from value, and then including all the values from the recursive call. We hit a base case when from is larger than to and we return an empty array.

There is a big possible eventual disadvantage to this. It's not tail-recursive. Functions in which any recursive call is immediately returned without further processing is called tail-recursive. There are some very useful optimizations on that JS engines can apply to tail-recursive functions to avoid overflowing the recursion stack.

We could make this tail-recursive by adding the default parameter or adding a wrapper function which passes that parameter to an internal recursive helper function. Either would be easy to do. However, we can see that it makes little difference in modern JS, since at the time of this answer almost no engines support proper tail calls, even though I believe doing so is still in the specification. So for now, we won't bother.

Here is this version in action:

const range = (from, to) =>
  from > to ? [] : [from, ... range (from + 1, to)]

console .log (range (1, 5)) //=> [1, 2, 3, 4, 5]
console .log (range (4, 4)) //=> [4]
console .log (range (5, 1)) //=> []
.as-console-wrapper {max-height: 100% !important; top: 0}
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • Your code is very eloquent but are you sure it's working properly ---> range (5, 1) from your code actually returns [ ] as oppose to [5,4,3,2,1] – noirsociety Sep 10 '21 at 18:03
  • 1
    It's working as intended, and it matches the original iterative sample. I don't know how and why I put that comment there. That test case was to distinguish from Jeremy Thille's answer which returns the strange result of `[5]`. I've fixed the comment now. – Scott Sauyet Sep 10 '21 at 18:14
  • Thank you Scott :) Your code is very eloquent and modern. More importantly, it actually kills 2 birds with one stone. – noirsociety Sep 10 '21 at 18:54
0

One workaround could be to add an accumulator to your recursive function, in order to bring in every steps the array you started at the beginning of the first call.

function recursive(index, length) {
  const result = [];
  result.push(index);
  return recursiveAcc(++index, length, result);
}

function recursiveAcc(index, length, result) {
  result.push(index);
  return (index < length) ? recursiveAcc(++index, length, result) : result;
}

console.log(recursive(1, 5)); // [ 1, 2, 3, 4, 5 ]
aaandri98
  • 585
  • 4
  • 18
  • 1
    you can get the same effect using just one function if you pass a default empty array for the `result` parameter. – Alnitak Sep 08 '21 at 14:54
  • (fwiw, the two function approach is popular with languages like Erlang where you can pick which function to call based on the arity of the parameters) – Alnitak Sep 08 '21 at 14:55