Class 26: Recursion II

Objectives for today

Review: How to Write a Recursive Function

  1. Define the function header, including the parameters
  2. Define the recursive case
  3. Define the base case
  4. Put it all together

Recursion with pending operations

Consider the following code. What is this code doing? What is the output of the call go_back(3)? (See lecture21.py for the docstring.)

def go_back(n):
    if n == 0:
        print("Stop")
    else:
        print("Go", n)
        go_back(n-1)
        print("Back", n)

The line print("Back", n) is an example of a “pending operation”, that is, an operation that gets performed when control continues after the recursive call. Check this code out in Python Tutor.

Limits on Recursion

Python has a limit on the height of the call stack (we saw that last time when we attempted “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 factorial(1000).

import sys
sys.setrecursionlimit(10000)

Recursive Drawing with Turtle

Spiral

Consider the following code to draw a spiral. In each iteration it draws a segment, turns left, and then recursively draws shorter segments (only 95% of the length).

def spiral1(length, levels):
    """
    Draw a spiral with 'levels' segments with initial 'length' 
    """
    # Implicit base case: do nothing if levels == 0
    if levels > 0:
        forward(length)
        left(30)
        spiral1(0.95 * length, levels-1) # Recurse 

Try running the code with say, spiral1(80, 45). Notice that the turtle stops and remains where the drawing ends at the inner tip of the spiral.

Let’s revise this code to “undo” the calls to forward and left. After the recursive call to spiral2(0.95 * length, levels-1), undo the left turn by turning right, and undo the forward by moving backward:

def spiral2(length, levels):
    """
    Draw a spiral with 'levels' segments with initial 'length' 
    """
    # Implicit base case: do nothing if levels == 0
    if levels > 0:
        forward(length)
        left(30)
        spiral2(0.95 * length, levels-1) # Recurse 
        right(30)
        backward(length)

If I started with the turtle at (0,0), what is the position of the turtle after invoke this function, e.g. spiral2(80, 45)? The turtle ends up back at (0,0) because we are “retracing” the movements after the recursive call. This is a common pattern in recursive functions in which we “reset” the state after a recursive call using state maintained in the call stack.

Drawing Trees and “Broccoli”

Check out the draw_tree and broccoli functions in lecture21.py:

def draw_tree(length, levels):
    """
    Draw a recursive tree and return to where the turtle started
    Args:
        length: length of initial tree trunk
        levels: number of tree trunk segments from bottom to top
    """
    if levels > 0:
        forward(length)                  # draw tree branch
        right(45)
        draw_tree(length/2, levels-1)    # draw right subtree
        left(45*2)                       # undo right turn, then turn left again
        draw_tree(length/2, levels-1)    # draw left subtree
        right(45)                        # undo left turn
        backward(length)                 # trace back down the tree branch

The above function draws nothing if levels is 0. This is another example of an implicit base case that simply returns (terminating execution) without doing anything.

For level == 1, the turtle moves forward, turns right, draws a “level 0” tree (i.e., nothing), turns left, draws another “level 0” tree (i.e., nothing), turns right so it is facing the original direction, then goes backward to undo the original forward.

For level == 2, the turtle moves forward, turns right, draws “level 1” tree (i.e., a line), turns left, draws another “level 1 “tree (i.e., a line), turns right so it is facing the original direction, then goes backward to undo the original forward.

At any level, the turtle moves forward, turns right, draws a level n-1 tree, turns left, draws another level n-1 tree, turns right so it is facing the original direction, then goes backward to undo the original forward.

Here are the trees drawn for levels 1, 2, 3, and 4:

Trees

Drawing the “broccoli” is very similar:

  1. Define the function header, including the parameters

    def broccoli(length)
    
  2. Define the recursive case

    Draw a line with three smaller broccolis at the end, one on the same heading, the other two offset by 20 degrees (by smaller we mean shorter “stem”).

  3. Define the base case

    Below a certain length, don’t draw a line (and more broccolis), just draw a dot.

  4. Put it all together

    Why do we include backward(length) at the end of the recursive function? This ensures that the turtle ends up where it started at the end of each subtree that is drawn. Notice that this backward command matches the forward command a few lines up, and these are the only movements of the turtle!

How many lines does this program draw? Lots! How could we figure out exactly how many? We could return the count of the number of lines drawn.

How do I make my problem smaller?

There is no “one” way to make the problem smaller. However we have seen several common patterns that we can use as we implement our recursive functions:

Understanding and Improving Efficiency

Let’s develop a recursive implementation for power, with parameter base and exp (exponent):

  1. Define the function header, including the parameters

    def power(base, exp)
    
  2. Define the recursive case

    \(base^{exp} = base * base^{(exp-1)}\)

  3. Define the base case

    \(base^0 = 1\)

  4. Put it all together

    def power(base, exp):
        if exp == 0:
            return 1
        else:
            return base * power(base, exp-1)
    

What is the time complexity of this implementation, i.e. how many times is power called? exp + 1 times, or linear with the size of exp.

What is the space complexity of this implementation, i.e. how will the memory grow as we increase exp? Also linearly. Why? Every time we invoke a function, Python creates a frame on the call stack. Each frame on the call stack requires some amount of space and the number of frames grows with the size of the input.

Can we do better?

Recall that \(base^{exp}=base^{exp/2}*base^{exp/2}\). So what if instead of recursing by exp-1, we recursed by dividing exp in half? Our new complexity could be logarithmic, specifically \(log_2 exp\).

We have to be careful though about how we handle even and odd exponents (we will need different recursive cases for each):

def power(base, exp):
    if exp == 0:
        return 1
    elif exp == 1:
        return base
    elif exp % 2 == 0:
        sub = power(base, exp//2)
        return sub * sub
    else:
        return base * power(base, exp-1)

Is this implementation still logarithmic? In the worst case the overhead is 2X (two calls for each division), and \(2log_2 n\) is still logarithmic.