####### Lecture 9 ####### ####### Recursion ####### # 1. Break a problem into smaller parts # 2. Evaluate these sub-parts # 3. Put everything back together # Recursion is usually written as a function # calling itself # Recursion generally DOES NOT involve for # or while loops # loops use ITERATIVE approach # recursive functions call themselves # All problems that can be solved with recursion # can also be solved iteratively, but recursion # is often easier to visualize and parallelize. ######################### # String reversal def reverseString(string): """returns string in reverse""" # Base case if string == '': return '' # This case lets us make one less recursive call, but is not necessary. ## if len(string) == 1: ## return string # Recursive case # sub-part: reverseString on all but end # putting it together: concatenation else: return string[-1] + reverseString(string[:-1]) #print reverseString('hello') # 'o' + 'lleh' ######################### # Fibonacci numbers # f(0) = f(1) = 1 # f(n) = f(n-1) + f(n-2) # memoization: storing intermediate fibCount = 0 # Start calculation with an empty memoization dictionary def fibMemo(n): return fibonacci(n, {}) # Uncomment print statements below to show # when f starts and finishes calculating. # Adapted from http://www.greenteapress.com/thinkpython/thinkCSpy/html/chap04.html def fibonacci(n, memoDict): """Calculates the nth fibonacci number""" # print "Calculating f of", n # Counting the number of calls global fibCount fibCount += 1 # Memoized solution: if we've done it, just look it up if n in memoDict: # print "f of", n, "already memoized" return memoDict[n] # Base cases if n == 0 or n == 1: return 1 # Recursive case else: # Sub-parts: fib(n-1), fib(n-2) # combination: addition result = fibonacci(n-1, memoDict) + fibonacci(n-2, memoDict) # Add the result to the dictionary memoDict[n] = result # print "Done calculating f of", n return result # fibCount shows number of recursive calls made to fibonacci() # Without memoizing, this would grow exponentially with n (bad)! # Instead, it grows linearly with n (good). print fibMemo(3), fibCount # fibCount = 0 # reset to 0 to get count for new call #print fibMemo(36), fibCount ########################### # Binary search ''' Given a sorted list and an element, return either an index where that element can be found, or None if one does not exist. ''' def binary_search(l, elem): # Base case: element is not in an empty list if len(l) == 0: return None # Base case 2: element is in the middle mid = len(l)/2 # len(1) = 0 -> mid = 0; integer division does this if l[mid] == elem: return mid # If not there, look in the half that would contain elem (recurse) if elem > l[mid]: sub = binary_search(l[mid+1:], elem) if sub is None: return None else: return mid + 1 + sub else: # elem < l[mid] return binary_search(l[:mid], elem) # test cases #print binary_search(range(10), 0) #print binary_search(range(10), 6) #print binary_search(range(10), 11) #print binary_search(range(10), -2) ########################### # Merge sort # Sorts a list, using the merge sort algorithm. def merge_sort(l): # Base case: length 0 and 1 lists are sorted if len(l) in (0,1): return l # Otherwise, split into 2 subproblems, sort recursively, and merge # split list into halves mid = len(l)/2 # integer division on purpose! lower = l[:mid] upper = l[mid:] # recursively sort halves lower = merge_sort(lower) upper = merge_sort(upper) # combine the sorted results return merge(lower, upper) # (resursive) helper method to merge already-sorted lists def merge(l1, l2): # if either list empty, return the other if len(l1) == 0: return l2; elif len(l2) == 0: return l1 # otherwise, pull off smaller element and keep going else: if l1[0] <= l2[0]: return [l1[0]] + merge(l1[1:], l2) else: return [l2[0]] + merge(l1, l2[1:]) # print merge_sort([5,4,7,2,8,4,1])