Week 10, Friday: Recursion

another example from last time

Here is the isSorted(L) function from last class:

def isSorted(L):
  """return True if list is in sorted order, low to high"""

  if len(L) <= 1:
    return True
  else:
    # check L[0] vs L[1]
    if L[0] > L[1]:
      return False
    else:
      return isSorted(L[1:])

For the base case, we know the answer: a zero- or one-element list is obviously sorted, so return True.

For the non-base case, just check if the first two are in order. If not, we can immediately say the list is not in order (return False). If the first two items are in order, make the leap of faith: isSorted(L[1:]) will give us a True or a False, if the rest of the list is sorted or not, so just return whatever we get back from that recursive call.

recursive graphics

Finally, now that we understand recursion a little better, here are some examples that are "naturally recursive". These are algorithms that are easier/more elegant to write using recursion. Again, you could write them iteratively (using loops), but sometimes it makes more sense to use recursion (not always!).

Here is an interesting picture. Can you see the recursion? What happens first, and what is the base case?

H recursion picture

Using Zelle Graphics, first write a function called H(win, pt, size) that draws a single "H" in the given graphics window, at the given Point pt, with the given size. For example, calling H(win,Point(100,100),50) would draw an "H" at the coordinates (100,100) with size 50.

For the "H" function, you will need to create Line() objects using other Point() objects. Using the clone() method will definitely help:

>>> from graphics import *
>>> pt = Point(100,100)      # assume we are given these
>>> size = 50                # assume we are given these
>>>
>>> p1 = pt.clone()
>>> p1.move(-size,-size)
>>> p2 = pt.clone()
>>> p2.move(-size,size)
>>> leftedge = Line(p1,p2)   # create left edge of the H
>>> leftedge.draw(win)

Once you have a working H() function, can you use it to draw one "H", then another at point p1? And another at that H's p1? And so on...

What is the base case for the recursion? What happens when the size gets too small? Also, how many times do we need to recur to get the four H's at the four corners (points p1, p2, and so on)?

more graphics examples

There are many interesting examples of recursive graphics algorithms. Take a look at some of these links and see if you can understand how they recur:

back to merge sort

Here is the mergeSort() algorithm from last week:

def mergeSort(L):
    """use merge sort to sort L from low to high"""
    if len(L) > 1:
      # split into two lists
      half = len(L)/2
      L1 = L[0:half]
      L2 = L[half:]
      # sort each list
      mergeSort(L1)
      mergeSort(L2)
      # merge them back into one sorted list
      merge(L1,L2,L)

For this one, the base case is not explicitly written. It just does "nothing" if the length of the list is one or less.

Any divide-and-conquer algorithms, like the merge sort, can usually be expressed easily using recursion.

analysis of merge sort

Since merge sort simply divides the original list in half, then each subsequent list in half, and so on, part of the big-O notation must be logN, because that's how many times we can divide a list of size N in half. The other part of merge sort is to merge them all back together. Given two sorted lists, the merge operation just compares the first items in each list. This will require at most N compares, so it's not hard to see that the merge sort is an O(NlogN) algorithm. This is better than all of the quadratic sorts we looked at last week (bubble, selection, and insertion sort).

Here is a nice picture from wikipedia of what happens during the merge sort.

And here are some timings (for initially random lists of N integers) of various sorts we have looked at:

$ python timesorts.py 
N: 2000
selection sort time:   0.1435
   bubble sort time:   0.4378
   python sort time:   0.0003
    merge sort time:   0.0090

$ python timesorts.py 
N: 4000
selection sort time:   0.5572
   bubble sort time:   1.7164
   python sort time:   0.0007
    merge sort time:   0.0188

Notice the time for the merge sort does not quadruple like the selection and bubble sort times. Also, python's built-in sort seems to be the best. What algorithm does the built-in sort use???