Cover page images (keyboard)

Introduction to Recursion

Sue Evans & Will Murnane



Hit the space bar for the next slide

Learning Outcomes

Functions

We have concentrated on top-down design and the use of functions so far in this course. You certainly have no problems writing functions at this point, but we still haven't examined all of the usefulness of functions.

Functions calling functions

You are quite aware that functions can call other functions, like this :

>>> def alpha(name):
...   print "Function alpha called with argument", name
...
>>> def beta(foo):
...   print "Function beta starting with argument", foo
...   alpha(foo)
...
>>> beta("Hello, World!")
Function beta starting with argument Hello, World!
Function alpha called with argument Hello, World!

Functions calling themselves

Functions aren't limited to calling functions with other names; they can also call themselves. Suppose we wanted to count down to some momentous occasion using a function. We could do this with a for or while loop, but for the sake of trying something new, let's try to implement this with a function that calls itself. Here's a first attempt:

>>> def countdown(n):
...   print n
...   countdown(n-1)
...
>>> countdown(10)
10 
9 
8 
7 
6 
5 
4 
3 
2 
1 
0 
-1 
-2 
-3 
(much more output)
  File ">stdin<", line 3, in countdown
  File ">stdin<", line 3, in countdown
  File ">stdin<", line 3, in countdown
  File ">stdin<", line 3, in countdown
RuntimeError: maximum recursion depth exceeded

Uh-oh. Something's wrong.
Let's look deeper to try to figure out what happened and how to fix it. We'll come back to this example in a few slides.

The call stack

Tracing a call stack

Let's try tracing through a program that has some function calls in it and see what the execution stack would look like at each stage.

>>> def one():
...   two()
...   three()
...
>>> def two():
...   four()
...   print "Done with two()"
...
>>> def three():
...   print "Almost done"
...
>>> def four():
...   print "Hello from four()!"
...
>>> one()

So we start with just one entry on the stack, which we'll call "interpreter". After all, after one() finishes executing, we'll be back in the interpreter. So, here's the stack right as we enter one():

interpreter

The next thing we do in one is call two(). So where we are is pushed onto the top of the stack:

one(), line 1
interpreter

Again, we immediately execute a function call in two, so where we are now is pushed onto the stack:

two(), line 1
one(), line 1
interpreter

Now we're in four, so we print the message "Hello from four()!". Now it's time to return from four, so we pop the first entry off the top of the stack and go to that address. Now we're back in two , and let's see the stack now.

one(), line 1
interpreter

So before we started with the function call, we were on line 1 of two. Now we'll move on to the next thing in two, which is a print statement again. We print the message, and we're ready to return from two. We pop the stack again and go to that address. Now we're in one, done with line 1 and about to execute line 2, and the stack contains only

interpreter

Then we have to call three, so we push our current location onto the stack:

one(), line 2
interpreter

Now we print the message in three and return to one. Since there's nothing left in one, we return one final time to the interpreter.

Diagnosing the earlier problem

Our earlier program

>>> def countdown(n):
...   print n
...   countdown(n-1)
...

complained about "maximum recursion depth exceeded". What happened here is we just kept calling functions (namely, countdown) until the size of the stack got too big to deal with and Python complained. countdown also has one fatal flaw: it kept counting even when the number was less than zero. This is one of the most important things to keep in mind with recursion:

Figure out how to stop first.

We must define the base case. This is the name given to the conditions under which the recursion stops.

In this case, when n reaches zero we want to print some kind of message. So we'll check for this condition before calling countdown again:

Recursive Definitions

Recursion in Math

A recursive definition is one which refers to itself as in these definitions of the factorial and fibonacci functions.

n! or fact(n) is defined as:

     fact(0) = 1  
     fact(n) = n * fact(n-1) , for  n > 0

the nth fibonacci number or fib(n) is defined as:

     fib(0) = 1
     fib(1) = 1
     fib(n) = fib(n-1) + fib(n-2) , for  n > 1 

Recursion in Programs

A recursive function is a function that calls itself.

The Factorial Function

def factorial (n):

   if n == 0:
      return 1

   else:
      return n * factorial(n - 1)

Comments on Recursion

Fibonacci

Leonardo Pisano Fibonacci, the Italian mathematician, is famed for his invention of the fibonacci sequence -- 1,1,2,3,5,8,13,21,34,... -- where each number is the sum of the previous two. The sequence appears in his work known as the "rabbit problem":

Fibonacci numbers in nature

The arrangement of leaves or twigs on a stem (phyllotaxis, from the Greek word phyhllon meaning leaf and taxis meaning arrangement) correspond to Fibonacci numbers. Select one leaf as a starting point and count up the leaves on the stem until you reach a leaf directly above your starting point. The number of leaves is usually a Fibonacci number. In the above figure, starting from the bottom leaf, we count up 5 leaves to find the next one directly above the bottom leaf. Also, you can count the number of turns around the stem, as you go from a leaf to one directly above it. This too is usually a Fibonacci number. For a pussy willow, typical numbers are 13 for the number of "leaves" and 5 times around.

Many web sites have fascinating discussions about Fibonacci Numbers in Nature. This is a particularly good one from World-Mysteries.

Recursive definition

   fib(n) = undefined for n < 0
   fib(0) = fib(1) = 1
   fib(n) = fib(n-1) + fib(n-2) for n > 1

Recursive function

def fib(n):

    # base case
    if n < 2:
        return 1

    # general rule
    else:
        return fib(n - 1) + fib(n - 2)

Iterative function

def fib(n):

  f1 = 1
  f2 = 1

  if n < 2:
    return 1 

  else:
    for i in range(2, n):
      temp = f1
      f1 = f2
      f2 = temp + f2

    return (f1 + f2)

Tracing Fibonacci

fib1

A recursive function for fibonacci with tracing.

fib1.py

# File: fib1.py
# Author: Richard Chang
# Date:  12/3/97
# Modified by Sue Evans - 11/19/09
# Section: All
# EMail: bogar@cs.umbc.edu
# 
# A recursive function for fibonacci with tracing.
 

# fib() recursively traces the fibonacci sequence
# for the nth number in the sequence
#
# Input: the number-place in the sequence to find
# Output: the value of the nth number in the sequence
# Side effect: prints tracing for finding the number
def fib(n):

   print "> fib(%d)" % (n)

   if n < 2:
       result = 1

   else:
      result = fib(n - 1) + fib(n - 2)

   print "< %d" % (result)

   return result



def main():

   n = -1

   while n < 1:
      n = input("Enter a positive integer: ")


   fib(n)


main()

Output

linuxserver1.cs.umbc.edu[111] python fib1.py
Enter a positive integer: 4
> fib(4)
> fib(3)
> fib(2)
> fib(1)
< 1
> fib(0)
< 1
< 2
> fib(1)
< 1
< 3
> fib(2)
> fib(1)
< 1
> fib(0)
< 1
< 2
< 5
linuxserver1.cs.umbc.edu[112]

Wow! I can't really see what's going on.

fib2

A recursive function for fibonacci with indented tracing.

fib2.py

# File: fib2.py
# Author: Richard Chang
# Date:  12/3/97
# Modified by Sue Evans - 11/19/09
# Section: All
# EMail: bogar@cs.umbc.edu
# 
# A recursive function for fibonacci with indented tracing.
 

# indent() prints vertical lines for the depth of 
# recursion in a recursive function to be traced
#
# Input: depth, will be the number of lines
# Output: [depth] printed lines, no return value
def indent(depth):

  for i in range(depth): 
    print "|  ",


# fib() recursively traces the fibonacci sequence
# for the nth number in the sequence
#
# Input: the number-place in the sequence to find
# Output: the value of the nth number in the sequence
# Side effect: prints tracing for finding the number
def fib(n, depth):

   indent(depth)
   print "> fib(%d)" % (n)

   if n < 2:
       result = 1

   else:
      result = fib(n - 1, depth + 1) + fib(n - 2, depth + 1)

   indent(depth)
   print "< %d" % (result)

   return result



def main():

   n = -1

   while n < 1:
      n = input("Enter a positive integer: ")


   fib(n, 0)



main()

Let's see if this helped!

linuxserver1.cs.umbc.edu[118] python fib2.py
Enter a positive integer: 4
> fib(4)
|   > fib(3)
|   |   > fib(2)
|   |   |   > fib(1)
|   |   |   < 1
|   |   |   > fib(0)
|   |   |   < 1
|   |   < 2
|   |   > fib(1)
|   |   < 1
|   < 3
|   > fib(2)
|   |   > fib(1)
|   |   < 1
|   |   > fib(0)
|   |   < 1
|   < 2
< 5
linuxserver1.cs.umbc.edu[119]

This is still pretty hard to understand. Let's trace it by hand!

Recursion Exercise

Multiplication

Realize that multiplication is simply repeated adding.
When we say a * b,
we mean to add a to its previous sum b times

Write a recursive function called multiply() that does multiplication by repeated adding.

Towers of Hanoi

There is a backstory involving Zen monks and 64 golden discs, but it's invented.

Animation

Here's a simple animation for the n=3 case.

A Recursive algorithm

Let's think about a recursive algorithm for solving the Towers of Hanoi

Preliminaries

For a start, let's consider a small number of discs: two.
To move these two discs from A to C:
we move the first disc from A to B,
then move the second disc from A to C,
then move the first disc from B to C.

To solve the Towers of Hanoi problem of size n with the intermediate peg "spare", moving the discs from "src" to "dest":

  1. we move n-1 discs from the source to the spare peg using the destination as an intermediate
  2. we move the last (largest) disc from the source to the destination
  3. we move the n-1 discs from the spare peg to the destination, using the source peg as an intermediate.

Translating this into the language of the original problem it becomes:

To move n discs from peg A to peg C:

  1. move n-1 discs from A to B.
  2. move the remaining disc from A to C.
  3. move n-1 discs from B to C.

Based on this algorithm, we can define the hanoi() function

# File:        hanoi.py
# Author:      Will Murnane
# Modified by: Sue Evans
# Date:        11/19/09
# Section:     All
# Email:       bogar@cs.umbc.edu
#
# This program solves the Towers of Hanoi puzzle
# using recursion


# move() prints the move from source to destination
#
# Inputs: src, the source peg
#         dest, the destination peg
# Output: None
def move(src, dest):
    print 'move ' + str(src) + '-->' + str(dest)


# hanoi() recusively solves the Towers of Hanoi problem
# for any positive n > 1
#
# Inputs: n, the number of discs
#         src, the source peg
#         spare, the extra peg
#         dest, the destination peg
# Output: None
def hanoi(n, src, spare, dest):

    # the base case
    if n == 0:
        return

    # the general rule

    # moves n - 1 discs from source to spare
    # using dest as intermediate
    hanoi(n - 1, src, dest, spare)

    # moves bottom disc from source to destination
    move(src, dest)

    # moves n - 1 discs from spare to destination
    # using the source as intermediate
    hanoi(n - 1, spare, src, dest)


def main():

    discs = -1

    while discs != 0:
        discs = input("Enter the number of rings (0 to end): ")
        
        hanoi(discs, 'A', 'B', 'C')


main()

Let's watch it run!

linuxserver1.cs.umbc.edu[132] python hanoi.py
Enter the number of rings (0 to end): 2
move A-->B
move A-->C
move B-->C
Enter the number of rings (0 to end): 3
move A-->C
move A-->B
move C-->B
move A-->C
move B-->A
move B-->C
move A-->C
Enter the number of rings (0 to end): 0
linuxserver1.cs.umbc.edu[133]

So what?

Fractals

A fractal is a geometric object which can be divided into parts, each of which is similar to the original object.

Koch snowflake

A Koch snowflake is a simple example.

It's the result of infinite additions of triangles to the perimeter of a starting triangle.

Each time new triangles are added (an iteration), the perimeter grows, and eventually approaches infinity.

In this way, the fractal encloses a finite area within an infinite perimeter.

Self similarity

A complex fractal shape emerges from the simple repetition of a basic shape at a smaller and smaller scale.

When you zoom in to a fractal, at every scale it appears the same.

If you "zoom in" on the infinite version of a Koch curve, you see more and more detail, but it all looks the same.

Here's an animation:

Nature seems fractal

Self-similarity makes fractals useful for modeling natural systems.

Consider the shape of a tree. From the trunk of a tree shoot off several branches.

Each branch then repeats this branching pattern and gives rise to smaller branches.

So the tree branching is self-similar.

some material adapted from MathBlues

Anagrams

Anagrams are popular puzzles where a jumbled word is shown and the player tries to find all of the words that are possible from those letters.

We're going to write an anagram solving program. We'll ask the user to enter a word or a jumbled word and we'll create all permutations of those letters and check the linux dictionary to see if any of those permutations are actually a word.

Considerations

Anagram Code

Here's anagrams.py

# Filename: anagrams.py
# Author:   Sue Evans
# Date:     11/22/09
# Section:  All
# Email:    bogar#cs.umbc.edu
#
# This program recursively generates all possible permutations
# of a 6-letter or less jumbled word entered by the user, then
# searches the dictionary using a recursive binary search for
# possible solutions.

 
import sys
import string


# anagrams() recursively produces a set of all possible
# permutations of the word passed in.
#
# Input:  str, the word
# Output: answer, the list of permutations
def anagrams(str):

    # base case 1
    if str == "":
        return [str]

    else:
        answer = []

        # general case
        # call anagrams passing all but the 1st letter
        for word in anagrams(str[1:]):
            for pos in range(len(word) + 1):
                answer.append(word[:pos] + str[0] + word[pos:])

        # base case 2
        return answer



# getDictList() reads in all of the words in the 
# dictionary file, strips them of trailing whitespace
# and changes them to all lower-case
#
# Input:  filename, the path and filename of the dictionary file
# Output: dictWords, the list of dictionary words 
def getDictList(filename):

    dictWords = []

    dictFile = open(filename, 'r')

    for line in dictFile:
        str = string.strip(line)
        str = string.lower(str)
        dictWords.append(str)
    
    dictFile.close()

    return dictWords



# binarySearch() searches recursively for an item in a list using
# the binary search algorithm.
#
# Inputs: item,  the item to be searched for
#         myList,  the list to search
#         left,  the left index
#         right, the right index
# Output: found, the index where item occurred in list or -1 if not found
def binarySearch(item, myList, left, right):

    # base case 1
    # no place left to look - not there
    if left > right:
        return -1

    mid = (left + right) / 2
    current = myList[mid]

    # base case 2
    # if item was found stop looking
    if current == item:
        return mid

    # general rule
    # look in lower half
    elif item < current:
        return binarySearch(item, myList, left,  mid - 1)
    # look in upper half
    else:
        return binarySearch(item, myList, mid + 1, right)



def main():

    MAX_LEN  = 6
    NUM_ARGS = 2

    # Check for correct number of command-line arguments
    argc = len(sys.argv)
    if argc != NUM_ARGS:
        print "This program requires a command line argument which is"
        print "the path to the linux dictionary"
        print "Usage: anagrams.py /usr/share/dict/words"
        sys.exit()

    # Get a word from the user that is less than
    # or equal to MAX_LEN characters
    word = ""
    while(len(word) < 1 or len(word) > MAX_LEN):
        word = raw_input("Enter a jumbled word of 6 or less characters : ")

    # Strip it of trailing whitespace & make it all lower-case
    word = string.strip(word)
    word = string.lower(word)

    # Get a list of all permutations of the word
    perms = anagrams(word)
    num = len(perms)

    # Get a list of the words in the dictionary file
    dictWords = getDictList(sys.argv[1])
    dictLen = len(dictWords)

    # Search the dictionary for each of the permutations
    # printing those that are actually words
    for i in range(num):
        found = binarySearch(perms[i], dictWords, 0, dictLen - 1)
        if found != -1:
            print perms[i]

main()

Let's run it!

linuxserver1.cs.umbc.edu[204] python anagrams.py /usr/share/dict/words
Enter a jumbled word of 6 or less characters : eap
ape
epa
pea
linuxserver1.cs.umbc.edu[205] python anagrams.py /usr/share/dict/words
Enter a jumbled word of 6 or less characters : lenag
nagle
nagel
lange
angle
angel
lagen
agnel
genal
elgan
glean
galen

Recursion vs. Iteration

Both recursion and iteration are important

Use recursion when

Use iteration when

Some recursive patterns are more efficient than others