So the key to this kind of thinking is understanding that you should be creating a lot of arrays.
Looking at a slightly different example...
A factorial is a number which goes backwards, through positive integers, multiplying each term with the term below it, and is written like 5!.
These are helpful when you find yourself asking questions like:
"How many permutations of ____ are there?"
"Given these 5 things, how many permutations can I arrange them in, from left to right?"
5! // =>
5 x 4 x 3 x 2 x 1 // =>
120
You could see how we could build a loop and set a variable for a counter, and a variable for the total, and multiply the current total by the current value of the counter we're decrementing.
But instead of doing that, we can try to use recursion.
First, think about how we could simplify that 5 x 4 x ... into one repeated step.
Really, 2! is 2 x 1. 3! is 3 x 2 x 1, which happens to be 3 x 2!.
So the general case might be something like: n! == n x (n - 1)!
So I might write a generalized function which does something like this:
// DO NOT RUN THIS FUNCTION!
function factorial (n) {
return n * factorial(n - 1);
}
So if I run factorial(5) and use my imagination, we can see that the program is doing something like:
factorial(5)
=> return 5 * factorial(5-1)
=> return 4 * factorial(4-1)
=> return 3 * factorial(3-1)
=> ...
Can you see any problems with the function as-is?
I said at the beginning that factorials (in this simplified case) are over positive integers.
How does my function know to stop when the integers stop being positive?
It doesn't, currently. Which is why the above implementation attempts to run forever, and will freeze the browser, while it tries to, until it gets thousands or tens of thousands of functions deep, before it says that you've reached the maximum depth of the call stack and explodes.
What we really need is a condition or a set of conditions, which we use to determine when we're done.
This is a base-case.
if (shouldStop(n)) {
return defaultValue;
}
Or in our case:
function factorial (n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
Now, when we run the function, we have:
factorial(5)
=> 5 * factorial(5 - 1)
=> 4 * factorial(4 - 1)
=> 3 * factorial(3 - 1)
=> 2 * factorial(2 - 1)
=> 1
=> 2 * 1
=> 3 * 2
=> 4 * 6
=> 5 * 24
=> 120
This is recursion.
And because of where the call is (returned at the very end of whatever branch you're in) it's a special kind of recursion (tail recursion), which allows some languages to optimize the code, replacing the function call with the contents of the function call, and thus skip adding to the call-stack like the first version (future versions of JS will support this power).
In more modern JS, I might rewrite it to look something like
const factorial = n => n <= 1 ? 1 : factorial(n - 1);
So now, what about other cases?
Well, sometimes, you need to make sure you're passing more things in.
Think about what your problem is, and what kinds of counters or flags or collectors you need, in order to do your job.
Here's one:
function makeNumberString (current, max, initialString) {
var str = initialString || ""; // maybe I don't have one yet
var currentString = str.concat(current.toString());
if (current > max) {
return initialString;
}
return makeNumberString(current + 1, max, currentString);
}
makeNumberString(0, 9); // "0123456789"
There are other ways of filling that function out, to make it do the same thing.
Note that currentString there is always a brand new string, made by joining the string that I was given with the new value I was passed. I'm not actually modifying the original string, but creating a new copy [HINT!!].
I hope that helps you.