CS 111 f21 — Recursion part 2

1 Review

What makes a function recursive?1

What is the role of a base case in a recursive function?2

2 Practice

  • rev_str("cat") should return "tac"
  • identify a base case (empty string) and how you will divide the work between the current call and the recursive call
def rev_str(s):
    if len(s) == 0:
        return ""
    rest_reversed = rev_str(s[:-1])
    return s[-1] + rest_reversed

2.1 Recursively Summing a List

def rec_sum(nums):
    if len(nums) == 0:
        return 0
    sum_of_the_rest = rec_sum(nums[1:])
    return nums[0] + sum_of_the_rest

3 When to Use Recursion

Oftentimes, if we can do something with a loop in Python, that will be preferable to a recursive solution. Why?

  • Recursive code can be more difficult to think through and debug
  • Making a recursive call is more work than doing another loop iteration
    • We can measure this empirically by timing how long a function takes to run
    • Consider our rec_sum function above versus this iteration version:

      def iter_sum(nums):
          result = 0
          while len(nums) > 0:
              result += nums[0]
              nums = nums[1:]
          return result
      
      • I deliberately wrote an iterative version that does the same slicing as the recursive version
    • Running python3 -m timeit --setup "from sums import rec_sum, iter_sum; a = list(range(100))" "rec_sum(a)" in the terminal asks Python to time how long it takes to call rec_sum on a list from 0 to 99. The output is:

      10000 loops, best of 5: 36.6 usec per loop
      

      meaning that Python ran rec_sum(a) 10000 times, and took the average of those. It repeated this 5 times, and reported the best of those. The result is 36.6 microseconds (usec) or 36 millionths of a second. So pretty fast!

    • Running Running python3 -m timeit --setup "from sums import rec_sum, iter_sum; a = list(range(100))" "iter_sum(a)" gives

      10000 loops, best of 5: 27.8 usec per loop
      

      indicating that iter_sum is somewhat faster on average.

    • We can also compare versions that avoid slicing:

      def rec_sum2(nums, i):
          if len(nums) == i:
              return 0
          sum_of_the_rest = rec_sum2(nums, i + 1)
          return nums[i] + sum_of_the_rest
      
      def iter_sum2(nums):
          result = 0
          for num in nums:
              result += num
          return result
      

      which result in

      20000 loops, best of 5: 17.6 usec per loop
      

      for rec_sum2 and

      100000 loops, best of 5: 3.57 usec per loop
      

      for iter_sum2.

    • Of all these versions, the one using a simple for loop is by far the fastest!
    • Takeaway: if you can implement something using a loop, that's probably the way to go. Save recursion for problems where it's necessary or clearly the right choice.

4 Fractal Art

An example of a sitution where recursion is clearly the right choice is drawing a fractal. A fractal is a shape where each part of the shape is a smaller version of the whole shape. The Koch Curve is one example.

We can use Python's turtle graphics to see how this works. This module lets us control the movement of a "turtle" that can draw lines as it moves. It's the same idea as the programs we wrote to draw our names the very first day of class:

import turtle

yertle = turtle.Turtle()
def draw_A(t):
    t.left(45)
    t.forward(200)
    t.right(90)
    t.forward(200)
    t.left(180)
    t.forward(100)
    t.left(45)
    t.forward(150)

yertle.speed(1)
draw_A(yertle)
turtle.mainloop()

The algorithm for a Koch Curve is

To draw a curve of length n:
    draw a curve of length n/3
    turn left 60 degrees
    draw a curve of length n/3
    turn right 120 degrees
    draw a curve of length n/3
    turn left 60 degrees
    draw a curve of length n/3

Two possibilites for a base case:

  1. stop when n is below some threshold
  2. add a parameter depth:

    To draw a curve of length n and depth d:
        if d is 0, draw a line of length n
        otherwise
            draw a curve of length n/3 and depth d - 1
            turn left 60 degrees
            draw a curve of length n/3 and depth d - 1
            turn right 120 degrees
            draw a curve of length n/3 and depth d - 1
            turn left 60 degrees
            draw a curve of length n/3 and depth d - 1
    
import turtle


def koch(t, n, depth):
    """Draws a koch curve of depth `depth` with length `n`."""
    if depth < 1:
        t.forward(n)
        return
    m = n/3
    koch(t, m, depth - 1)
    t.left(60)
    koch(t, m, depth - 1)
    t.right(120)
    koch(t, m, depth - 1)
    t.left(60)
    koch(t, m, depth - 1)


yertle = turtle.Turtle()
yertle.penup()
yertle.goto(-250, -200)
yertle.pendown()
yertle.hideturtle()
yertle.speed(0)
for i in range(6):
    koch(yertle, 300, i)
    yertle.penup()
    yertle.goto(-250, -200 + 100 * (i + 1))
    yertle.pendown()
turtle.mainloop()

koch.png

5 A Recursive Class

  • a recursive data structure because each instance contains a reference to another instance
class PersonInLine:
    def __init__(self, name):
        self.name = name
        self.next_person = None
    def __repr__(self):
        if self.next_node != None:
            return self.name + " -> " + repr(self.next_person)
        return self.name

def make_line(names):
    if len(names) == 0:
        return None
    start = PersonInLine(names[0])
    start.next_person = make_line(names[1:])
    return start

h = make_linked_list(["Listo", "Mr. Dictionary", "Queen Tuple", "The String"])
print(h)

Footnotes:

1

It calls itself as part of its definition.

2

To end the recursion by describing the conditions where there is no recursive call.