"e"[1:]''
Recursion
Tower of Hanoi is a classic puzzle in which you need to transfer a set of discs from one pole to another pole using a spare pole. It has three simple rules:
Try playing the game here!
How can we solve this problem? There is definitely some iteration here, but the iterative tools we have seen (for and while loops) might be hard to use at first glance. With problems like this, it’s a good idea to think about:
Let’s break up our problem into smaller pieces. The smallest piece (or problem “size”) is a single disk. And we know how to solve that! Just move the disk over to another pole.
If we currently have \(n\) disks, we can move the top \((n-1)\) disks over (assuming we know how to do that), displace the bottom disk to the remaining empty pole, and then displace the top \((n-1)\) disks again on top of the bottom disk.
The main thing to focus on is the fact that we are going to use our solution to the smaller problem in order to solve our current problem.
factorialAs a starting example consider computing the factorial, e.g, \(n!\). A natural iterative solution is below.
A recursive algorithm is defined in terms of solutions to smaller versions of the same problem. A recursive function (which implements a recursive algorithm) calls itself to solve a smaller version of the problem.
Let’s think about solving factorial recursively. Can we expression the factorial computation in terms of the solution to a smaller version of the same problem.
5! = 5 * 4 * 3 * 2 * 1
5! = 5 * 4!
A first attempt at a recursive factorial function:
Let’s visualize the call stack:
5 * factorial(4)
|
4 * factorial(3)
|
3 * factorial(2)
|
2 * factorial(1)
|
1 * factorial(0)
|
0 * factorial(-1)
|
...
So when will this end? Never! At some point we need to terminate the recursion. We call that the base case. The base case and the recursive relationship are the two key elements of any recursive algorithm.
For factorial, we know that factorial(1) == 1 (and factorial(0) == 1) so:
Here we see the typical structure of a recursive function: First we check if we are at the base case(s), if so return the result directly. If not, we invoke the function recursively on a sub-problem.
Clearly this works. But why? Doesn’t each call to factorial overwrite n? No. To help us understand what happens when we call a function (and what we mean by the call stack) let’s use Python Tutor on our factorial function. Whenever we invoke a function we create a new “frame” on the “call stack” that contains the arguments (local variables and other state in the function). That is n in each recursive call is a different variable. Thus we don’t “overwrite” the parameters when we repeatedly invoke our function.
Python has a limit on the height of the call stack (that we will encounter if we ever end up with “infinite” recursion). Depending on how many recursive calls you make, e.g. factorial(1000), you may hit that limit triggering a RecursionError (even without infinite recursion). You can increase the limit with a function in the sys module, like shown below, thus enabling us to successfully compute very “deep” recursive functions, e.g., factorial of very large numbers.
We employ a 4 step process:
Recursion has a similar feel to “induction” in mathematics:
Let’s use this process to recursively reverse a string (check it out in Python Tutor):
Define the function header, including the parameters
Define the recursive case
Assume we have a working reverse function that can only be called on smaller strings. To reverse a string:
Define the base case
The reverse of the empty string is just the empty string.
Put it all together
An implementation note … Why doesn’t a_string[1:] produce an index error when a_string is a single letter (e.g. "e"[1:])? Slicing has the nice property that slicing beyond the end of the string evaluates to the empty string, e.g.
However, indexing a single value (not slicing) beyond the end of a string (or list) will produce an error, e.g.
The recursive case expresses the solution of our problem in terms of smaller version of the same problem. But how do we make our problem “smaller”? While, there is no “one” way to make the problem smaller, we have seen several common patterns that we can use as we implement our recursive functions:
a_list[1:] would produce a smaller list by “dropping” the first element.What happens if we don’t make our problem smaller? The function never makes progress towards the base case, recursing infinitely (or still it hits the Python recursion limit).
Consider the following code. What is this code doing? What, for example, is the output of the call go_back(3)?
The line print("Back", n) is an example of a “pending operation”. A pending operation gets performed after a/the recursive call, i.e., when control continues after the recursive call. Check this code out in Python Tutor. Pending operations can be a powerful tool when performing operations as a recursive function “unwinds” (i.e., after the base case has been reached).
A palindrome is a word that appears the same when read forwards or backwards. Here are some examples:
Here is a possible method which uses a for loop to determine if a word is a palindrome:
Write a recursive function to determine if an input string is a palindrome.
Challenge: can you extend this to handle palindrome phrases in which punctuation and spaces are ignored? Palindrome phrases (the function should return True) would include:
Hint: use the string replace method to remove punctuation (i.e. replace punctuation characters with an empty string ''). The only punctuation we have in the examples above are ,.-! but you can generalize this by using string.punctuation from the string module.