Recursion - an example with steps

Posted by Mallory Feuer on May 7, 2019

Recursion is the concept of a function calling itself either directly or indirectly. To illustrate some important points about recursion, let’s look at how we would refactor an iterative solution to become a recursive one.

Let’s first develop an interative solution to a steps problem. The directions state that we must write a function that will console log a step shape with n levels using the # character.

Examples:

steps(2)
'# '
'##'
	 
steps(3)
'#  '
'## '
'###'

So the solution depends on finding a pattern that determines when we should add a # and when we should add a blank space to our stair string. Making a diagram can help us to visualize the pattern here: if the column number is less than or equal to the row number, we should add a #, else just a space.

n = 3 Column 0 Column 1 Column 2
Row 0 # - -
Row 1 # # -
Row 2 # # #

iterative solution:

function steps(n) {
   for (let row = 0; row < n; row++) {
	    let stair = ' ';
			
			for (let column = 0; column < n; column++){
			   if (column <= row) {
			      stair += '#';
			   } else {
				  stair += ' ';
			  }
			}  
			
			console.log(stair);
	 }
}

A few general points on recursion before we refactor this iterative solution to be recursive:

  • The very first thing we should do when developing a recursive solution is to come up with a base case. This describes the condition when there is no more work for the function to do, and it’s time to return and stop the recursion process. Getting the base case established early on will allow us to run and test our solution without creating an infinite loop.
  • We want to call our function again with different arguments, the recursive step. It is very important to make sure we have changed the arguments in some way on each function call, and that this change will lead us to the base case. If we do not change the arguments, we’ll never reach the base case, and it’ll cause an infinite loop.
  • We also have to think about reasonable defaults. If there are any optional inputs, we should give them default values so that we can safely handle calling our function with our without these arguments without entering an infinite loop.

Refactor!

So now, in order to refactor our iterative solution, the first step is identifying our base case. If row === n, this means we are at the end of our problem. This is something we noticed in our iterative solution as well since we iterated through each row up to n in our for loop.

It is also important to think about some other patterns from our iterative solution. We can note that if the stair string has a length === n, then we are at the end of a row, and we’re ready to console log. If the length of the stair string is less than or equal to the row number we’re working on, we add a # , otherwise add a space.

function steps(n) {
   if (n === row) {  // base case
	    return;
   }
}

So here is our base case as we described earlier. However, right now there is no variable or argument called row. We can add row as an argument to our steps function to specify the current row we are on. We also need to make sure (as we mentioned earlier) that we give row a default argument, just in case it is not defined in our function call. Otherwise, we might get some really unexpected behavior. We can default the value of row to 0 so we will start on the very first row.

function steps(n, row = 0) {
   if (n === row) {  // base case
	    return;
   }
}

Then we have to write some code to see if the length of the stair string is equal to n, and if it is, we console log out the value of stair.

function steps(n, row = 0) {
   if (n === row) {  // base case
	    return;
   }
	 
   if (n === stair.length) {  // if we are at the end of the row
	    console.log(stair)
   }
}

However, like the base case, we don’t have a variable called stair right now. So we can also add stair as an argument as well because it’s another piece of information it would be useful to have with each function call. Again, we want to give it a default value to make sure we protect ourselves from calling the function with an undefined argument. The default value we’ll give stair in this case will be an empty string '', which represents the beginning of the stair.

function steps(n, row = 0, stair = '') {
   if (n === row) {  // base case
	    return;
   }
	 
   if (n === stair.length) {  // if we are at the end of the row
	    console.log(stair)
   }
}

Now, the only thing we have left to figure out is how we want to call our steps function again, and how we will make changes to stair and row so we will eventually reach our base case. One thing we know: we need to increment row when we reach the end of a row–like when we complete a stair!

function steps(n, row = 0, stair = '') {
   if (n === row) {  // base case
	    return;
   }
	 
   if (n === stair.length) {  // if we are at the end of the row
	   console.log(stair);
	   steps(n, row + 1)  // increment row
	   return;
   }
}

So here we are calling our steps function again with row + 1 to increment our row variable, but we are not passing it any value for stair. Here we are relying on our default value for stair, which is an empty string. At this stage, when we have just finished a row, and we console logged out the stair value, we want stair to reset to an empty string with which we can start our next row. Since there is nothing left to do here, we can return.

Now that we’ve handled our base case, and the case where we’ve hit the end of a row, what’s left is to handle the case where we are still assembling our stair string. This is where we use the pattern that if the length of stair is less than or equal to the value of row, we add #. Otherwise add a space.

function steps(n, row = 0, stair = '') {
   if (n === row) {  // base case
	    return;
   }
	 
   if (n === stair.length) {  // if we are at the end of the row
	    console.log(stair);
	    steps(n, row + 1)  // increment row
	    return;
   }
	 
   if (stair.length <= row) {
	    stair += '#';
   } else {
	    stair += ' ';
   }
}

So here we’ve added some logic to deal with building out each stair, but at this point we’re only recursing when we hit the end of a row. We still need to recurse through the row as well. So after we have added a character (# or ' '), then we need to call our steps function again. This time, we’ll call our steps function with the current value of row because we are not changing rows, and we’ll pass it our current value of stair. So with each function call, unless our value of row is equal to n (base case), or the length of stair is equal to n (end of a row), we will add a value onto stair, which will increase its length.

function steps(n, row = 0, stair = '') {
   if (n === row) {  // base case
	    return;
   }
	 
   if (n === stair.length) {  // if we are at the end of the row
	   console.log(stair);
	   steps(n, row + 1)  // increment row
	   return;
	 }
	 
   if (stair.length <= row) {
	    stair += '#';
   } else {
	    stair += ' ';
   }
	 
   steps(n, row, stair);
}

Phew! Ok, so here we have it. Thank you for reading, and I hope this was helpful.