CS 111 f21 — Recursion part 2
1 Review
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 callrec_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)"
gives10000 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
and100000 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:
- stop when
n
is below some threshold 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()
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)