pdb
and %debug
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:
class Bag:
def __init__(self, price, *contents):
self.price = price
self.contents = contents
bag1 = Bag(10)
def price(bag):
return bag.price
price(bag1)
bag2 = Bag(5, Bag(3))
price(bag2) #issue: not compute Bag(3)
def price(bag): #bag inside bag
p = bag.price
for b in bag.contents:
p += b.price
return p
bag2 = Bag(5, Bag(3))
price(bag2)
def price(bag): #bag inside bag inside bag
p = bag.price
for b in bag.contents:
p += b.price
for c in b.contents:
p += c.price
return p
bag3 = Bag(5, Bag(4, Bag(3)), Bag(2))
price(bag3)
bag4 = Bag(0, Bag(5), Bag(10), Bag(3, Bag(2), Bag(100)), Bag(9, Bag(2, Bag(25))))
#recursive solution
def price(bag):
p = bag.price
for b in bag.contents:
#code here
p += price(b)
return p
price(bag1)
price(bag2)
price(bag3)
price(bag4)
import sys
sys.setrecursionlimit(200) #define the max allowed depth of stack
def silly_rec(n):
print(n)
silly_rec(n)
silly_rec(1) #issues?
def silly_rec(n):
print(n)
if n == 0:
# base case: no recursion
print('Base case 1 hit')
elif n == -100:
# base case: no recursion
print('Base case 2 hit')
else:
# recursive case: must make progress towards a base case
silly_rec(n-1)
silly_rec(1)
silly_rec(10)
silly_rec(-10)
$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$
def rec_factorial(n):
print('n = ', n)
if n == 0: #base case
return 1
else:
return n * rec_factorial(n-1)
rec_factorial(10)
$m + n = \begin{cases} m & \text{if}\ n=0 \\ (m + 1) + (n - 1) & \text{if}\ n > 0 \end{cases}$
def add(m, n):
print('m, n = ', (m, n))
if n == 0:
return m
else:
return add(m+1, n-1)
add(5, 0)
add(5, 1)
add(5, 5)
def bin_search(x, lst):
mid = len(lst)//2
print(lst)
if not lst: #base case 1, not found the item
return False
elif lst[mid] == x: #base case 2, found the item
return True
elif lst[mid] < x:
return bin_search(x, lst[mid+1:])
else: # lst[mid] > x
return bin_search(x, lst[:mid])
bin_search(20, list(range(100)))
bin_search(-1, list(range(100)))
bin_search(50.5, list(range(100)))
$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, ...
def rec_fib(n):
#print('n = ', n)
if n == 0:
return 0
elif n == 1:
return 1
else:
return rec_fib(n-1) + rec_fib(n-2)
rec_fib(5)
rec_fib(6)
[rec_fib(i) for i in range(20)]
rec_fib(35)
rec_fib(36)
n = 2**40
print(n)
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):
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?
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): # move N discs (i.e. levels) from Rod "frm" to Rod "to" using the Rod "using"
if levels == 1:
move(frm, to)
else:
hanoi(frm, using, to, levels-1)
move(frm, to)
hanoi(using, to, frm, levels-1)
towers
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)
display()
#move(0,2)
#hanoi(0, 2, 1, 3)
hanoi(0, 2, 1, 5)
def merge(l1, l2): # O(N), where N is the number of elements in the two lists
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
l1 = [1, 5, 9]
l2 = [2, 6, 8, 11]
merge(l1, l2)
def mergesort(lst):
if len(lst) <= 1:
return lst
else:
mid = len(lst)//2
return merge(mergesort(lst[:mid]), mergesort(lst[mid:]))
import random
lst = list(range(10))
random.shuffle(lst)
lst
mergesort(lst)
def mergesort(lst):
if len(lst) <= 1:
return lst
else:
mid = len(lst)//2
l1 = mergesort(lst[:mid])
l2 = mergesort(lst[mid:])
#print('Merging', l1, 'and', l2)
return merge(l1, l2)
mergesort(lst)
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
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
def add(self, x):
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:
heap.add(x)
sorted_lst = []
while heap:
sorted_lst.append(heap.pop_max())
sorted_lst.reverse()
return sorted_lst
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)'.format(size),
setup='import random ; from __main__ import mergesort ; '
'lst = [random.random() for _ in range({})]'.format(size),
number=1))
%matplotlib inline
import matplotlib.pyplot as plt
#plt.plot(insertionsort_times, 'ro')
plt.plot(heapsort_times, 'b^')
plt.plot(mergesort_times, 'gs')
plt.show()
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?
def change(amount, denoms):
if amount == 0:
return 1
elif amount < 0 or not denoms:
return 0
else:
return (change(amount-denoms[0], denoms) + change(amount, denoms[1:]))
change(5, (1, 5, 10, 25))
change(10, (1, 5, 10, 25))
change(10, (25, 10, 5, 1))
change(100, (25, 10, 5, 1))
# print out the solutions
def ways_change(amount, denoms, way=()):
if amount == 0:
print(way)
return 1
elif amount < 0 or not denoms:
return 0
else:
return (ways_change(amount-denoms[0], denoms, way = way + (denoms[0],))
+ ways_change(amount, denoms[1:], way))
ways_change(10, (1, 5))
ways_change(25, (1, 5, 10, 25))
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$ + the number of ways we can make $(N−d_i)$ using all $k$ denominations
[Note] Dynamic Programming Solution, instead of recursive call on same state, save results of state in a table and look them up.
factorial
¶class Stack(list):
push = list.append
pop = lambda self: list.pop(self, -1)
peek = lambda self: self[-1]
empty = lambda self: len(self) == 0
#simulate a call stack
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): #save a local variable, avoid being overwritten
call_stack.push(('local', name, val))
def restore_local(): #return a local variable
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]
# simulate all the calls that goes down to the base case
call(10) # initial call (with argument 10)
while True: # recursive calls
n = get_arg()
if n == 1:
return_with(1)
break
else:
save_local('n', n)
call(n-1)
call_stack
ret = last_return_val()
n = restore_local()
return_with(n * ret)
call_stack
def rec_factorial(n):
if n == 0: #base case
return 1
else:
return n * rec_factorial(n-1)
rec_factorial(10)
pdb
and %debug
¶import sys
sys.setrecursionlimit(100)
def rec_factorial(n):
if n <= 1: # detect base case
raise Exception('base case!')
else:
return n * rec_factorial(n-1)
rec_factorial(10)
%debug
# commands to try:
# help, where, args, p n, up, u 10, down, d 10, l, up 100, u, d (& enter to repeat)
# The most convenient interactive interface to debugging is the %debug magic command.
# If you call it after hitting an exception,
# it will automatically open an interactive debugging prompt at the point of the exception.
# The ipdb prompt lets you explore the current state of the stack,
# explore the available variables, and even run Python commands!
pdb - The typical usage to break into the debugger from a running program is to insert import pdb; pdb.set_trace() at the location you want to break into the debugger.
You can then step through the code following this statement, and continue running without the debugger using the continue command.
list - show code
continue - run to next break
next - execute current line, NOT stopping within functions in that line
step - execute current line, stopping within functions in that line
p expression - print the expression
args - Print the argument list of the current function.
quit
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:])
bin_search(20, list(range(100)))