# On Recursion¶

## Agenda¶

1. Recursion
• stopping recursion: simplification & base cases
2. Recursive "shapes":
• Linear (single) recursion:
• Factorial
• Binary search
• Tree (multiple) recursion: divide and conquer
• Fibonacci numbers
• Tower of Hanoi
• Merge sort
• Making change
3. The Call Stack and Stack Frames
• simulating recursion
• debugging with pdb and %debug

## 1. Recursion¶

Recursive functions, directly or indirectly, call themselves.

Recursive solutions are applicable when a problem can be broken down into more easily solved sub-problems that resemble the original, and whose solutions can then be combined.

E.g., computing the combined price of a bunch of nested shopping bags of items:

In [1]:
class Bag:
def __init__(self, price, *contents):
self.price = price
self.contents = contents

In [2]:
bag1 = Bag(10)

In [3]:
bag2 = Bag(5, Bag(3))

In [4]:
bag3 = Bag(5, Bag(4, Bag(3)), Bag(2))

In [5]:
bag4 = Bag(0, Bag(5), Bag(10), Bag(3, Bag(2), Bag(100)), Bag(9, Bag(2, Bag(25))))

In [6]:
def price(bag):
p = bag.price
for b in bag.contents:
p += price(b)
return p

In [7]:
price(bag4)

Out[7]:
156

### Stopping recursion: simplification & base case(s)¶

In [8]:
import sys
sys.setrecursionlimit(100)

In [9]:
def silly_rec(n):
if n == 0: # base case
return
n = n - 1 # simplify
silly_rec(n)

In [10]:
silly_rec(10)


To help visualize the recursive process, we can simply substitute the value of the parameter in each recursive call.

E.g., for silly_rec(5):

silly_rec(5) -> silly_rec(4) -> silly_rec(3) -> silly_rec(2) -> silly_rec(1) -> silly_rec(0) # encounter base case

## 2. Recursive "shapes"¶

### Linear recursion¶

#### Example: Factorial¶

$n! = \begin{cases} 1 & \text{if}\ n=0 \\ n \cdot (n-1)! & \text{if}\ n>0 \end{cases}$

i.e., $n! = n \cdot (n-1) \cdot (n-2) \cdots 3 \cdot 2 \cdot 1$

In [11]:
def rec_factorial(n):
print('n = ', n)
if n == 0:
return 1
else:
return n * rec_factorial(n-1)

rec_factorial(10)

n =  10
n =  9
n =  8
n =  7
n =  6
n =  5
n =  4
n =  3
n =  2
n =  1
n =  0

Out[11]:
3628800

#### Example: Addition of two positive numbers $m$, $n$¶

$m + n = \begin{cases} m & \text{if}\ n=0 \\ (m + 1) + (n - 1) & \text{if}\ n > 0 \end{cases}$

In [12]:
def add(m, n):
print('m, n = ', (m, n))
if n == 0:
return m
else:

In [13]:
add(5, 0)

m, n =  (5, 0)

Out[13]:
5
In [14]:
add(5, 1)

m, n =  (5, 1)
m, n =  (6, 0)

Out[14]:
6
In [15]:
add(5, 5)

m, n =  (5, 5)
m, n =  (6, 4)
m, n =  (7, 3)
m, n =  (8, 2)
m, n =  (9, 1)
m, n =  (10, 0)

Out[15]:
10
In [16]:
def bin_search(x, lst):
if len(lst) == 0:
return False
else:
print('lo, hi = ', (lst[0], lst[-1]))
mid = len(lst) // 2
if x == lst[mid]:
return True
elif x < lst[mid]:
return bin_search(x, lst[:mid])
else:
return bin_search(x, lst[mid+1:])

In [17]:
bin_search(20, list(range(100)))

lo, hi =  (0, 99)
lo, hi =  (0, 49)
lo, hi =  (0, 24)
lo, hi =  (13, 24)
lo, hi =  (20, 24)
lo, hi =  (20, 21)
lo, hi =  (20, 20)

Out[17]:
True
In [18]:
bin_search(-1, list(range(100)))

lo, hi =  (0, 99)
lo, hi =  (0, 49)
lo, hi =  (0, 24)
lo, hi =  (0, 11)
lo, hi =  (0, 5)
lo, hi =  (0, 2)
lo, hi =  (0, 0)

Out[18]:
False
In [19]:
bin_search(50.5, list(range(100)))

lo, hi =  (0, 99)
lo, hi =  (51, 99)
lo, hi =  (51, 74)
lo, hi =  (51, 62)
lo, hi =  (51, 56)
lo, hi =  (51, 53)
lo, hi =  (51, 51)

Out[19]:
False

### Tree recursion¶

#### Example: Fibonacci numbers¶

$fib(n) = \begin{cases} 0 & \text{if}\ n=0 \\ 1 & \text{if}\ n=1 \\ fib(n-1) + fib(n-2) & \text{otherwise} \end{cases}$

i.e., 0, 1, 1, 2, 3, 5, 8, 13, 21, ...

In [20]:
def rec_fib(n):
print('n = ', n)
if n == 0 or n == 1:
return n
else:
return rec_fib(n-1) + rec_fib(n-2)

rec_fib(5)

n =  5
n =  4
n =  3
n =  2
n =  1
n =  0
n =  1
n =  2
n =  1
n =  0
n =  3
n =  2
n =  1
n =  0
n =  1

Out[20]:
5

#### Example: Tower of Hanoi¶

Setup: three rods, with one or more discs of different sizes all stacked on one rod, smallest (top) to largest (bottom). E.g.,

     ||          ||          ||
==          ||          ||
====         ||          ||
======        ||          ||
------------------------------------



Goal: move all the discs, one by one, to another rod, with the rules being that (1) only smaller discs can be stacked on larger ones and (2) only the top disc in a stack can be moved to another rod.

For three discs, as shown above, we would carry out the following sequence to move the stack to the rightmost rod. The rods are abbreviated L (left), M (middle), R (right):

1. Move the small disc (0) from L to R
2. Move the medium disc (1) from L to M
3. Move 0 from R to M (R is empty)
4. Move the large disc (2) from L to R
5. Move 0 from M to L
6. Move 1 from M to R
7. Move 0 from L to R (done)

Can you come up with the sequence needed to move a stack of 4 discs from one rod to another? 5 discs? An arbitrary number of discs?

In [21]:
height = 5
towers = [[] for _ in range(3)]
towers[0] = list(range(height, 0, -1))

def move(frm, to):
towers[to].append(towers[frm].pop(-1))
display()

def hanoi(frm, to, using, levels):
if levels == 1:
move(frm, to)
else:
hanoi(frm, using, to, levels-1)
move(frm, to)
hanoi(using, to, frm, levels-1)

In [22]:
towers

Out[22]:
[[5, 4, 3, 2, 1], [], []]
In [23]:
from time import sleep
from IPython.display import clear_output

def display():
clear_output(True)
print('{:^12}'.format('||') * 3)
for level in range(height, 0, -1):
for t in towers:
try:
print('{:^12}'.format('==' * t[level-1]), end='')
except IndexError:
print('{:^12}'.format('||'), end='')
print()
print('-' * 36)
sleep(1)

In [24]:
hanoi(0, 2, 1, 5)

     ||          ||          ||
||          ||          ==
||          ||         ====
||          ||        ======
||          ||       ========
||          ||      ==========
------------------------------------


#### Example: Mergesort¶

In [6]:
def merge(l1, l2):
merged = []
i1 = i2 = 0
while i1 < len(l1) or i2 < len(l2):
if i2 == len(l2) or (i1 < len(l1)
and l1[i1] < l2[i2]):
merged.append(l1[i1])
i1 += 1
else:
merged.append(l2[i2])
i2 += 1
return merged

In [7]:
l1 = [1, 5, 9]
l2 = [2, 6, 8, 11]
merge(l1, l2)

Out[7]:
[1, 2, 5, 6, 8, 9, 11]
In [8]:
def mergesort(lst, lo, hi):
if lo > hi:
return []
elif lo == hi:
return lst[lo:lo+1]
else:
mid = (lo + hi) // 2
return merge(mergesort(lst, lo, mid),
mergesort(lst, mid+1, hi))

In [9]:
import random
lst = list(range(10))
random.shuffle(lst)

In [10]:
mergesort(lst, 0, len(lst))

Out[10]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [11]:
def insertion_sort(lst):
for i in range(1, len(lst)):
for j in range(i, 0, -1):
if lst[j-1] > lst[j]:
lst[j-1], lst[j] = lst[j], lst[j-1] # swap
else:
break

In [12]:
class Heap:
def __init__(self):
self.data = []

@staticmethod
def _parent(idx):
return (idx-1)//2

@staticmethod
def _left(idx):
return idx*2+1

@staticmethod
def _right(idx):
return idx*2+2

def _heapify(self, idx=0):
while True:
l = Heap._left(idx)
r = Heap._right(idx)
maxidx = idx
if l < len(self) and self.data[l] > self.data[idx]:
maxidx = l
if r < len(self) and self.data[r] > self.data[maxidx]:
maxidx = r
if maxidx != idx:
self.data[idx], self.data[maxidx] = self.data[maxidx], self.data[idx]
idx = maxidx
else:
break

self.data.append(x)
i = len(self.data) - 1
p = Heap._parent(i)
while i > 0 and self.data[p] < self.data[i]:
self.data[p], self.data[i] = self.data[i], self.data[p]
i = p
p = Heap._parent(i)

def max(self):
return self.data[0]

def pop_max(self):
ret = self.data[0]
self.data[0] = self.data[len(self.data)-1]
del self.data[len(self.data)-1]
self._heapify()
return ret

def __bool__(self):
return len(self.data) > 0

def __len__(self):
return len(self.data)

def heapsort(iterable):
heap = Heap()
for x in iterable:
sorted_lst = []
while heap:
sorted_lst.append(heap.pop_max())
sorted_lst.reverse()
return sorted_lst

In [18]:
import timeit
import random
insertionsort_times = []
heapsort_times = []
mergesort_times = []
for size in range(100, 3000, 100):
insertionsort_times.append(timeit.timeit(stmt='insertion_sort(lst)',
setup='import random ; from __main__ import insertion_sort ; '
'lst = [random.random() for _ in range({})]'.format(size),
number=1))
heapsort_times.append(timeit.timeit(stmt='heapsort(lst)',
setup='import random ; from __main__ import heapsort ; '
'lst = [random.random() for _ in range({})]'.format(size),
number=1))
mergesort_times.append(timeit.timeit(stmt='mergesort(lst, 0, {}-1)'.format(size),
setup='import random ; from __main__ import mergesort ; '
'lst = [random.random() for _ in range({})]'.format(size),
number=1))

In [20]:
%matplotlib inline
import matplotlib.pyplot as plt
plt.plot(insertionsort_times, 'ro')
plt.plot(heapsort_times, 'b^')
plt.plot(mergesort_times, 'gs')
plt.show()

In [21]:
%matplotlib inline
import matplotlib.pyplot as plt
plt.plot(heapsort_times, 'b^')
plt.plot(mergesort_times, 'gs')
plt.show()


#### Example: Making Change¶

Question: how many different ways are there of making up a specified amount of money, given a list of available denominations?

E.g., how many ways of making 10 cents, given 1c, 5c, 10c, 25c coins?

Key observation: the number of ways we can make up some amount $N$ using $k$ different denominations is equal to:

• the number of ways we can make $N$ excluding some denomination $d_i$, plus
• the number of ways we can make $(N - d_i)$ using all $k$ denominations
In [3]:
def change(amount, denoms):
if amount == 0:
return 1
elif amount < 0 or not denoms:
return 0
else:
return (change(amount, denoms[1:])
+ change(amount-denoms[0], denoms))

In [5]:
change(5, (1, 5, 10, 25))

Out[5]:
2
In [7]:
change(100, (1, 5, 10, 25))

Out[7]:
242

Followup question: can we list the actual ways of making change?

In [10]:
# approach 1: build a tuple on the way "down" the recursion
def change(amount, denoms, way=()):
if amount == 0:
print(way)
return 1
elif amount < 0 or not denoms:
return 0
else:
return (change(amount, denoms[1:], way)
+ change(amount-denoms[0], denoms, way + (denoms[0],)))

In [11]:
change(5, (1, 5, 10, 25))

(5,)
(1, 1, 1, 1, 1)

Out[11]:
2
In [12]:
change(10, (1, 5, 10, 25))

(10,)
(5, 5)
(1, 1, 1, 1, 1, 5)
(1, 1, 1, 1, 1, 1, 1, 1, 1, 1)

Out[12]:
4
In [15]:
# approach 2: build a list of tuples on the way back "up" the recursion
def change(amount, denoms):
if amount == 0:
return [()]
elif amount < 0 or not denoms:
return []
else:
ways = []
ways.extend(change(amount, denoms[1:]))
ways.extend([w + (denoms[0],) for w in change(amount-denoms[0], denoms)])
return ways

In [16]:
change(10, (1, 5, 10, 25))

Out[16]:
[(10,), (5, 5), (5, 1, 1, 1, 1, 1), (1, 1, 1, 1, 1, 1, 1, 1, 1, 1)]

## 3. The Call Stack¶

### Simulating recursive factorial¶

In [35]:
class Stack(list):
push = list.append
pop  = lambda self: list.pop(self, -1)
peek = lambda self: self[-1]
empty = lambda self: len(self) == 0

In [36]:
call_stack = Stack()

def call(arg):
call_stack.push('<frame begin>')
call_stack.push(('arg', arg))

def get_arg():
return call_stack.peek()[-1]

def save_local(name, val):
call_stack.push(('local', name, val))

def restore_local():
return call_stack.pop()[2]

def return_with(val):
while call_stack.pop() != '<frame begin>':
pass
call_stack.push(('ret', val))

def last_return_val():
return call_stack.pop()[-1]

In [37]:
call(10) # initial call
while True: # recursive calls
n = get_arg()
if n == 1:
return_with(1)
break
else:
save_local('n', n)
call(n-1)

In [38]:
call_stack

Out[38]:
['<frame begin>',
('arg', 10),
('local', 'n', 10),
'<frame begin>',
('arg', 9),
('local', 'n', 9),
'<frame begin>',
('arg', 8),
('local', 'n', 8),
'<frame begin>',
('arg', 7),
('local', 'n', 7),
'<frame begin>',
('arg', 6),
('local', 'n', 6),
'<frame begin>',
('arg', 5),
('local', 'n', 5),
'<frame begin>',
('arg', 4),
('local', 'n', 4),
'<frame begin>',
('arg', 3),
('local', 'n', 3),
'<frame begin>',
('arg', 2),
('local', 'n', 2),
('ret', 1)]
In [47]:
ret = last_return_val()
n = restore_local()
return_with(n * ret)
call_stack

Out[47]:
[('ret', 3628800)]

### Debugging with pdb and %debug¶

In [48]:
import sys
sys.setrecursionlimit(100)

In [49]:
def rec_factorial(n):
if n <= 1:   # detect base case
raise Exception('base case!')
else:
return n * rec_factorial(n-1)

In [50]:
rec_factorial(10)

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-50-f914dbc0d6f8> in <module>()
----> 1 rec_factorial(10)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
3         raise Exception('base case!')
4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
3         raise Exception('base case!')
4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
3         raise Exception('base case!')
4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
3         raise Exception('base case!')
4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
3         raise Exception('base case!')
4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
3         raise Exception('base case!')
4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
3         raise Exception('base case!')
4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
3         raise Exception('base case!')
4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
3         raise Exception('base case!')
4     else:
----> 5         return n * rec_factorial(n-1)

<ipython-input-49-0eb6f9e46905> in rec_factorial(n)
1 def rec_factorial(n):
2     if n <= 1:   # detect base case
----> 3         raise Exception('base case!')
4     else:
5         return n * rec_factorial(n-1)

Exception: base case!
In [ ]:
%debug
# commands to try:
# help, where, args, p n, up, u 10, down, d 10, l, up 100, u, d (& enter to repeat)

In [51]:
def bin_search(x, lst):
if len(lst) == 0:
return False
else:
print('lo, hi = ', (lst[0], lst[-1]))
mid = len(lst) // 2
if x == lst[mid]:
import pdb ; pdb.set_trace()
return True
elif x < lst[mid]:
return bin_search(x, lst[:mid])
else:
return bin_search(x, lst[mid+1:])

In [53]:
bin_search(20, list(range(100)))

lo, hi =  (0, 99)
lo, hi =  (0, 49)
lo, hi =  (0, 24)
lo, hi =  (13, 24)
lo, hi =  (20, 24)
lo, hi =  (20, 21)
lo, hi =  (20, 20)
> <ipython-input-51-0c8fd9d61981>(9)bin_search()
-> return True
(Pdb) p mid
0
(Pdb) q

---------------------------------------------------------------------------
BdbQuit                                   Traceback (most recent call last)
<ipython-input-53-435836e6f8af> in <module>()
----> 1 bin_search(20, list(range(100)))

<ipython-input-51-0c8fd9d61981> in bin_search(x, lst)
9             return True
10         elif x < lst[mid]:
---> 11             return bin_search(x, lst[:mid])
12         else:
13             return bin_search(x, lst[mid+1:])

<ipython-input-51-0c8fd9d61981> in bin_search(x, lst)
9             return True
10         elif x < lst[mid]:
---> 11             return bin_search(x, lst[:mid])
12         else:
13             return bin_search(x, lst[mid+1:])

<ipython-input-51-0c8fd9d61981> in bin_search(x, lst)
11             return bin_search(x, lst[:mid])
12         else:
---> 13             return bin_search(x, lst[mid+1:])

<ipython-input-51-0c8fd9d61981> in bin_search(x, lst)
11             return bin_search(x, lst[:mid])
12         else:
---> 13             return bin_search(x, lst[mid+1:])

<ipython-input-51-0c8fd9d61981> in bin_search(x, lst)
9             return True
10         elif x < lst[mid]:
---> 11             return bin_search(x, lst[:mid])
12         else:
13             return bin_search(x, lst[mid+1:])

<ipython-input-51-0c8fd9d61981> in bin_search(x, lst)
9             return True
10         elif x < lst[mid]:
---> 11             return bin_search(x, lst[:mid])
12         else:
13             return bin_search(x, lst[mid+1:])

<ipython-input-51-0c8fd9d61981> in bin_search(x, lst)
7         if x == lst[mid]:
8             import pdb ; pdb.set_trace()
----> 9             return True
10         elif x < lst[mid]:
11             return bin_search(x, lst[:mid])

<ipython-input-51-0c8fd9d61981> in bin_search(x, lst)
7         if x == lst[mid]:
8             import pdb ; pdb.set_trace()
----> 9             return True
10         elif x < lst[mid]:
11             return bin_search(x, lst[:mid])

/usr/local/Cellar/python3/3.5.2_3/Frameworks/Python.framework/Versions/3.5/lib/python3.5/bdb.py in trace_dispatch(self, frame, event, arg)
46             return # None
47         if event == 'line':
---> 48             return self.dispatch_line(frame)
49         if event == 'call':
50             return self.dispatch_call(frame, arg)

/usr/local/Cellar/python3/3.5.2_3/Frameworks/Python.framework/Versions/3.5/lib/python3.5/bdb.py in dispatch_line(self, frame)
65         if self.stop_here(frame) or self.break_here(frame):
66             self.user_line(frame)
---> 67             if self.quitting: raise BdbQuit
68         return self.trace_dispatch
69

BdbQuit: