Table of Contents
Overview:
- Terms to know: Recursive case, terminating case
- Concepts: Iterative approach vs. recursive approach
1. Examples of recursion in math
Recursion is a manner of solving a problem by breaking it down into smaller pieces - and those smaller pieces are solved in the same way as the big-picture version.
1.1. Example: Summation
A summation can be broken down into smaller chunks but using the same structure as the original.
Let's say we have \[ \sum_{i=1}^{6} { i } = 1 + 2 + 3 + 4 + 5 + 6 \]
The summation can be redefined recursively:
\[ \sum_{i=1}^{6} { i } = 6 + \sum_{i=1}^{5} { i } \]
The summation from \(i = 1\) to \(6\) is equivalent to whatever the sum is at \(i=6\), plus the sum from \(i = 1\) to \(5\).
We can continue this way until we get to a case that we know (e.g., \(\sum_{i=1}^{1} { i }\)).
- \(\sum_{i=1}^{6} { i } = 6 + \sum_{i=1}^{5} { i }\)
- \(\sum_{i=1}^{5} { i } = 5 + \sum_{i=1}^{4} { i }\)
- \(\sum_{i=1}^{4} { i } = 4 + \sum_{i=1}^{3} { i }\)
- \(\sum_{i=1}^{3} { i } = 3 + \sum_{i=1}^{2} { i }\)
- \(\sum_{i=1}^{2} { i } = 2 + \sum_{i=1}^{1} { i }\)
- We know that \(\sum_{i=1}^{1} { i } = 1\), then we move back up to sub out this value.
| Recursive problem | |||
|---|---|---|---|
| A. \[\sum^{6}_{i=1} { i } = 6 + \sum_{i=1}^{5} { i }\] | But what is | \[\sum_{i=1}^{5} { i }\] | ? |
| B. \[\sum_{i=1}^{5} { i } = 5 + \sum_{i=1}^{4} { i }\] | But what is | \[\sum_{i=1}^{4} { i }\] | ? |
| C. \[\sum_{i=1}^{4} { i } = 4 + \sum_{i=1}^{3} { i }\] | But what is | \[\sum_{i=1}^{3} { i }\] | ? |
| D. \[\sum_{i=1}^{3} { i } = 3 + \sum_{i=1}^{2} { i }\] | But what is | \[\sum_{i=1}^{2} { i }\] | ? |
| E. \[\sum_{i=1}^{2} { i } = 2 + \sum_{i=1}^{1} { i }\] | But what is | \[\sum_{i=1}^{1} { i }\] | ? |
| F. \[\sum_{i=1}^{1} { i } = 1\] |
Now we start substituting this value back for each step…
| Finding the solution | ||
|---|---|---|
| G. \[\sum_{i=1}^{2} { i } =\] | \[2 + \sum_{i=1}^{1} { i } =\] | \[2 + 1 = 3\] |
| H. \[\sum_{i=1}^{3} { i } =\] | \[3 + \sum_{i=1}^{2} { i } =\] | \[3 + 3 = 6\] |
| I. \[\sum_{i=1}^{4} { i } =\] | \[4 + \sum_{i=1}^{3} { i } =\] | \[4 + 6 = 10\] |
| J. \[\sum_{i=1}^{5} { i } =\] | \[5 + \sum_{i=1}^{4} { i } =\] | \[5 + 10 = 15\] |
| K. DONE! \[\sum_{i=1}^{6} { i } =\] | \[6 + \sum_{i=1}^{5} { i } =\] | \[6 + 15 = 21\] |
1.2. Example: Factorials
With a factorial of \(n\), written \(n!\) the formula to solve this is:
\[ n! = n \cdot (n-1) \cdot ... \cdot 3 \cdot 2 \cdot 1 \]
So,
- \(2!\) is \(2 \cdot 1\),
- \(3!\) is \(3 \cdot 2 \cdot 1\),
- \(4!\) is \(4 \cdot 3 \cdot 2 \cdot 1\), and so on.
Additionally, we can break down each of these equations: \(3!\) is equivalent to \(3 \cdot 2!\) \(4!\) is equivalent to \(4 \cdot 3!\) … \(n!\) is equivalent to \(n \cdot (n-1)!\)
Thinking of breaking down the problem here in this way is looking at it recursively.
Review 1. What is the result of 6! ?
Review 2. What is the result of n! ?
Review 3. What is the result of (n-1)! ?
2. Recursion basics
In programming, we usually approach problems iteratively, using a for-loop or a while-loop:
int Sum( int n ) { int result = 0; for ( int i = 1; i <= n; i++ ) { result += i; } return result; }
Which solves this summation:
\[ \sum_{i=1}^n {i} \]
But some types of problems lend themselves better to a recursive solution. To be fair, though, many problems are better solved iteratively. So how do we know which method is better?
When defining a problem recursively in programming, we need two things:
- A terminating case: A case that ends our recursing. Often, this is some known data, something hard-coded. For example with our summation, the terminating case would be that \[ \sum_{i=1}^1 {i} = 1 \] or for a factorial, \(1! = 1\) and \(0! = 1\).
- A recursive case: A recursive case is what happens
otherwise - if we're not to a solution yet (via the terminating case),
we call the same function again, but with updated arguments. For example:
Factorial( 4 ) = 4 * Factorial( 3 )Factorial( 3 ) = 3 * Factorial( 2 )Factorial( 2 ) = 2 * Factorial( 1 )Factorial( 1 ) = 1
We can solve these basic math operations both iteratively and recursively:
Factorial - Iterative solution
int FactorialI( int n ) { int result = 1; for ( int i = 1; i <= n; i++ ) { result *= i; } return result; }
Factorial - Recursive solution
int FactorialR( int n ) { if ( n == 1 || n == 0 ) { return 1; } return n * FactorialR( n-1 ); }
Review 4. What will be returned for FactorialR( 3 )?
return ___ * Factorial( ___ );
Review 5. What will be returned for FactorialR( 4 )?
return ___ * Factorial( ___ );
Review 6. What will be returned for FactorialR( n )?
return ___ * Factorial( ___ );
3. Breaking down problems into recursive solutions
One of the most challenging parts of recursion, at least for me, is trying to break away from thinking of something in terms of "looping" and figuring out how to think of it "recursively". It's not as natural-feeling, so don't worry if it's confusing at first.
Let's tackle some basic design problems to practice.
3.0.1. Example: Summation
Try to convert the Summation function to be recursive. Think about what the terminating case would be and the recursive case. Use the Factorial function for reference.
Summation - Iterative solution
int SumI( int n ) { int result = 0; for ( int i = 1; i <= n; i++ ) { result += i; } return result; }
Review 7. When does the loop terminate?
Summation - Recursive starter
int SumR( int n ) { // Terminating case? // Recursive case? }
Review 8. If we call Sum( 2 ) we are expecting the result to be…
If we call Sum( 3 ) we are expecting the result to be…
Review 9. If we call Sum( n ) we are expecting the result to be…
Summation - Recursive solution
int SumR( int n ) { if ( n == 1 ) { return 1; } // Terminating case return n + SumR( n-1 ); // Recursive case }
3.0.2. Example: Draw a line
Now let's make a function that will draw a line of symbols, with a parameter being the length. Iteratively, it could look like this:
DrawLine - Iterative solution
void DrawLineI( int amount ) { for ( int i = 0; i < amount; i++ ) { cout << "-"; } }
How would we repeat this behavior recursively? How do we have a "count up" sort of functionality? What would be the terminating case?
We're going to think of it a little differently: The recursive function will only output one "-" before it recurses. Each time it recurses, it draws one more dash…
DrawLine - Recursive work in progress
void DrawLine_Recursive( int amount ) { cout << "-"; // Recursive case DrawLineR( amount ); }
However, we don't have a terminating case… it will continue looping, but it won't go forever like a bad while loop. We will eventually run out of stack space and the program will encounter a stack overflow and end.
So what would the terminating case be? How do we adjust the amount
each time? Since amount is the one parameter we have, let's
have the recursion stop once it is 0. Each time we recurse, we can
pass in amount-1 to the next call…
DrawLine - Recursive solution
void DrawLine_Recursive( int amount ) { cout << "-"; // Terminating case if ( amount == 0 ) { return; } // Recursive case DrawLineR( amount - 1 ); }
3.0.3. Example: Counting up
How can we write a function that takes a start and end integer,
and outputs each number between them (including the start and end)?
Iteratively, it could look like this:
CountUp - Iterative solution
void CountUpI( int start, int end ) { for ( int i = start; i <= end; i++ ) { cout << i << "\t"; } }
CountUp - Recursive starter
Try to fill in this function to build a recursive solution:
void CountUpR( int start, int end ) { }
CountUp - Recursive solution
void CountUpR( int start, int end ) { cout << start << "\t"; // Terminating case if ( start == end ) { return; } // recursive case CountUp_Recursive( start+1, end ); }
4. A case for recursion
Although there are a lot of problems we could convert from an iterative solution to a recursive solution, there are some types of problems that really are better suited to recursion.
4.1. Example: Searching a File System
On a harddrive, we generally have files and folders. Folders can contain files, but they will also contain subfolders as well. And subfolders can each contain their own subfolders.
When you don't know the exact layout of the filesystem, how would you even begin to iteratively search for a specific file?
Instead, it is good to think of it recursively. For example, say we're searching for a folder where you store your Recursion homework. We will begin searching at the top-most folder of the computer. The algorithm would then run like…
Let's say we have a recursive function, Find( FOLDERNAME, currentFolder ).
For this functionality, terminating cases would be:
- Have we found the item? Return it.
- Are we out of places to search? Return nothing.
And the recursive case would be:
- Has a subfolder? Call Find() on that folder.
So looking at an example set of folders and files like this:
C:
|-- games/
| |--DOOM.exe
|-- school/
| |-- cs210/
| |-- cs235/
| |-- recursion.txt
|-- work/
|-- TPSreports/
So steps to locate the "Recursion" folder would look like this:
Find( "recursion.txt", "C:" ) Does "C:" have the recursion.txt file? NO... Does it have subfolders? YES... OK, Recurse. | | Find( "recursion.txt", "C:\games" ) | Does "C:\games" have the recursion.txt file? NO... | Does it have subfolders? YES... OK, Recurse. | | | | Find( "recursion.txt", "C:\games\DOOM.exe" ) | | Does "C:\games\DOOM.exe" match "recursion.txt"? NO... | | Does it have subfolders? NO... Return. | | | Does "C:\games" have any more subfolders? NO... Return. | Does "C:" have any more subfolders? YES... OK, Recurse. | | Find( "recursion.txt", "C:\school" ) | Does "C:\school" have the recursion.txt file? NO... | Does it have subfolders? YES... OK, Recurse. | | | | Find( "recursion.txt", "C:\school\cs210" ) | | Does "C:\school\cs210" have the recursion.txt file? | | NO... Does it have subfolders? NO... Return. | | | Does "C:\school" have any more subfolders? YES... OK, Recurse. | | | | Find( "recursion.txt", "C:\school\cs235" ) | | Does "C:\school\cs235" have the recursion.txt file? *YES*... | | OK, return this path. | | | "C:\school\cs235" path was returned, continue returning it. | "C:\school\cs235" path was returned, continue returning it.
And the result for finding the recursion folder is "C:\school\cs235".
4.2. Example: Solving a maze
Solving a maze can be approached from a recursive standpoint much easier than an iterative one.
Terminating cases:
- If we get to the end of the maze, SUCCESS.
- If we hit a dead-end, GO BACK (return).
Recursive case: Explore each available direction, except the direction you came from.
- Try to go NORTH
- Try to go EAST
- Try to go SOUTH
- Try to go WEST
Example step-through, going from A to B (@) is current location.
######## Path so far: ### ## # Recursive case: Can't go NORTH, can go EAST... A @ ## Z ### # ######## ######## Path so far: E ### ## # Recursive case: Can't go NORTH, can go EAST... A @ ## Z ### # ######## ######## Path so far: E E ### ## # Recursive case: Can't go NORTH, can go EAST... A @ ## Z ### # ######## ######## Path so far: E E E ### ## # Recursive case: Can go NORTH... A @## Z ### # ######## ######## Path so far: E E E N ###@## # Recursive case: Can't go NORTH, can't go EAST, A ## Z can't go WEST... ### # Return to previous location... ######## ######## Path so far: E E E ### ## # Recursive case: Already went NORTH. A @## Z Can't go EAST... Can go SOUTH... ### # ######## ######## Path so far: E E E S ### ## # Recursive case: Can go EAST... A ## Z ###@ # ######## ######## Path so far: E E E S E ### ## # Recursive case: Can go EAST... A ## Z ### @ # ######## ######## Path so far: E E E S E E ### ## # Recursive case: Can go EAST... A ## Z ### @ # ######## ######## Path so far: E E E S E E E ### ## # Recursive case: Can go NORTH... A ## Z ### @# ######## ######## Path so far: E E E S E E E N ### ## # Recursive case: Can go NORTH... A ##@ Z ### # ######## ######## Path so far: E E E S E E E N N ### ##@# Recursive case: Can't go NORTH, can't go EAST, A ## Z can't go WEST. ### # Return to previous location... ######## ######## Path so far: E E E S E E E N ### ## # Recursive case: Can go EAST... A ##@ Z ### # ######## ######## Path so far: E E E S E E E N E ### ## # Made it to the end! A ## @Z Return "EEESEEENE" as the solution! ### # ########