2017.11.07

Recursive Functions

• In its body, a function may call other functions.

• One of those functions may be itself.

• A function that calls itself in the process of computing its result is called a recursive function.

def countdown (n) :
"""
signature: int -> NoneType
precondition: n >= 0
"""
if n == 0 :            # a base case
print ('go!')
elif n > 0 :
print (str (n))
countdown (n - 1)  # a recursive call
• There is a conditional analyzing the argument(s) to decide whether the function should call itself.

• A branch where the function doesn’t call itself is a base case.

• A self-call of the function is a recursive call.

Example: Countdown

Many programming problems can be solved by using either iteration or recursion:

def countdown_iter (n) :
while n > 0 :
print (str (n))
n -= 1
print ('go!')

$n \quad ⟼ \quad \overbrace{n , \underbrace{(n−1) , (n−2) , ⋯ , 1 , \mathrm{go!}} _ {\mathrm{countdown} ~ (n−1)} } ^ {\mathrm{countdown} ~ n}$

def countdown_rec (n) :
if n == 0 :             # how to count down from zero:
print ('go!')
elif n > 0 :            # how to count down from a positive number,
print (str (n))
countdown_rec (n - 1)   # assuming we know how to count down from its predecessor

Iteration vs Recursion

• iteration solves problems by building up solutions using loops and accumulators or effects.

• recursion solves problems by breaking them down into similar but smaller problems with recursive function calls.

• iteration makes progress as loop variables get “closer” to failing the guard condition.

• recursion makes progress as the arguments to recursive calls get “closer” to a base case.

• iteration requires the programmer to structure subproblem solution composition. (“manual bookkeeping”)

• recursion uses the language’s function-calling mechanism to build up solutions from subproblem solutions. (“automatic bookkeeping”)

• iteration tackles problems from a “global” perspective.

• recursion tackles problems for a “local” perspective.

Recursion lets us “think locally and act globally”.

Example: Factorial

"""
signature: int -> int
precondition: n >= 0
returns the factorial of the argument
"""
def factorial_iter (n) :                     # from lecture 4
acc = 1
while n > 0 :
acc *= n
n -= 1
return acc

$n \quad ⟼ \quad \overbrace{n × \underbrace{(n−1) × (n−2) × ⋯ × 1} _ {\mathrm{factorial} ~ (n−1)} } ^ {\mathrm{factorial} ~ (n)}$

def factorial_rec (n) :
if n == 0 :                                     # compute the factorial of zero:
return 1
elif n > 0 :                                    # compute the factorial of a positive number,
factorial_of_pred = factorial_rec (n - 1)   # assuming we know the factorial of its predecessor:
return n * factorial_of_pred

A common strategy for writing recursive functions for lists:

• A list is either empty ([]), or else it’s not. This often provides the case distinction for recursive functions on lists.

• An empty list has no smaller slices, so it must be a base case.

• A nonempty list must have at least one element, so there is always an element at index 0. This element is called the list head:

head = xs [0]
• A nonempty list also has a slice beginning at index 1 and ending at the end of the list. This slice is called the list tail:

tail = xs [1 : ]
• The list tail is a list that is one element shorter than the original list. In this sense it’s “smaller”.

• Often, a recursive call is made on the list tail.

This strategy also works for strings.

Example: Recursive List Summing

"""
signature: list (int) -> int
sums the numbers in a list
"""
def sum_iter (ns) :
acc = 0
for n in ns :
acc += n
return acc

$[n_0 , n_1 , n_2 , ⋯ , n_k] \quad ⟼ \quad \overbrace{n_0 + \underbrace{n_1 + n_2 + ⋯ + n_k} _ {\mathrm{sum} ~ (ns [1:])} } ^ {\mathrm{sum} ~ (ns)}$

def sum_rec (ns) :
if ns == [] :                       # sum of an empty list of numbers:
return 0
else :                              # sum a non-empty list of numbers,
tail_sum = sum_rec (ns [1 : ])  # assuming we know how to sum its tail:
return ns [0] + tail_sum

Example: Recursive List Reversal

"""
signature: any a . list (a) -> list (a)
reverses a list of any type
"""
def reverse_iter (xs) :                       # from lecture 6
acc = []
for x in xs :
acc = [x] + acc
return acc

$[x_0 , x_1 , x_2 , ⋯ , x_n] \quad ⟼ \quad [x_n, ⋯ , x_2 , x_1 , x_0] ~ = ~ \overbrace{\underbrace{[x_n, ⋯ , x_2 , x_1]} _ {\mathrm{reverse} ~ (xs [1:])} + [x_0] } ^ {\mathrm{reverse} ~ (xs)}$

def reverse_rec (xs) :
if xs == [] :                                   # reverse an empty list:
return []
else :                                          # reverse a non-empty list,
reversed_tail = reverse_rec (xs [1 : ])     # assuming we know how to reverse its tail:
return reversed_tail + [xs [0]]

Recursive Specifications

A palindrome is a word that reads the same forward as backward.

We can characterize the property of being a palindrome with the following specification:

• The empty string is a palindrome.

• A non-empty string is a palindrome if the first and last characters are equal and the rest of the characters, excluding the first and last, themselves form a palindrome.

This specification is recursive because in the second case we refer to the same property that we are describing.

It’s easy to turn this recursive specification into a recursive predicate function.

def is_palindrome (text) :
if text == '' :
return True
else :
return text [0] == text [-1] and is_palindrome (text [1 : -1])

Self-Similarity

A self-similar shape is one whose description is recursive, i.e. it contains a reference to itself.

Very simple recursive descriptions can produce interesting self-similar shapes

def tree (size , depth) :
# signature:  int , int -> NoneType
# precondition:  size >= 0 and depth >= 0
if depth == 0 :
pass
elif depth > 0 :
turtle.forward (size)
turtle.left (30.0)
tree (4/5 * size , depth - 1)
turtle.right (60.0)
tree (4/5 * size , depth - 1)
turtle.left (30.0)
turtle.back (size)

Here’s a helper-function for speeding up deeply-nested recursive drawings:

def faster (drawer , *args) :
delay = turtle.delay ()
frames = turtle.tracer()
turtle.tracer (300 , 100)
drawer (*args)
turtle.update ()
turtle.tracer (frames , delay)

e.g.

faster (tree , 100 , 12)  # a faster version of: tree (100 , 12)

Fractal Triangles

Starting with an equilateral triangle

we can draw a scaled-down version of it in each of its three corners

and scaled down version of those in each of their corners

and so on and so on…

The result is called a Sierpinski triangle:

def sierpinski_triangle (size , depth) :
# signature:  int , int -> NoneType
# precondition:  size >= 0 and depth >= 0
if depth == 0 :
pass
elif depth > 0 :
for i in range (3) :                            # for each of the three sides of a triangle:
sierpinski_triangle (size / 2 , depth - 1)  # draw a scaled-down fractal triangle
turtle.forward (size)                       # draw an edge of the current triangle
turtle.left (120.0)                         # turn to create a corner of the current triangle

Once we have figured out its recursive structure, we can optimize it by pruning the empty branch:

def sierpinski_triangle (size , depth) :
if depth > 0 :
for i in range (3) :
sierpinski_triangle (size / 2 , depth - 1)
turtle.forward (size)
turtle.left (120.0)