Recursion, Part 2


  1. File System Navigation
    1. printFiles
    2. listFiles
    3. removeTempFiles
  2. Fractals
    1. kochSnowflake
    2. sierpinskiTriangle
  3. Backtracking
    1. maze solving
    2. nQueens
  4. Memoization (optional)

  1. File System Navigation
    Folders can contain folders or files. Since folders can contain other folders, they are a recursive data structure. In fact, they are a kind of recursive structure called a tree (where each value has exactly one parent, and there is a topmost or "root" value). Traversing such a recursive data structure is a natural use of a recursive algorithm!

    These programs only run locally (not in the browser), and require that you first download and expand sampleFiles.zip in the same folder as the Python file you are running.

    1. printFiles
      import os def printFiles(path): # Base Case: a file. Just print the path name. if os.path.isfile(path): print(path) else: # Recursive Case: a folder. Iterate through its files and folders. for filename in os.listdir(path): printFiles(path + '/' + filename) printFiles('sampleFiles') # Note: if you see .DS_Store files in the sampleFiles folders, or in the # output of your function (as often happens with Macs, in particular), # don't worry: this is just a metadata file and can be safely ignored.

    2. listFiles
      import os def listFiles(path): if os.path.isfile(path): # Base Case: return a list of just this file return [ path ] else: # Recursive Case: create a list of all the recursive results from # all the folders and files in this folder files = [ ] for filename in os.listdir(path): files += listFiles(path + '/' + filename) return files print(listFiles('sampleFiles'))

    3. removeTempFiles
      Note: Be careful when using os.remove(): it's permanent and cannot be undone!
      That said, this can be handy, say to remove .DS_Store files on Macs, and can be modified to remove other kinds of files, too. Just be careful.
      import os def removeTempFiles(path, suffix='.DS_Store'): if path.endswith(suffix): print(f'Removing file: {path}') os.remove(path) elif os.path.isdir(path): for filename in os.listdir(path): removeTempFiles(path + '/' + filename, suffix) removeTempFiles('sampleFiles') # removeTempFiles('sampleFiles', '.txt') # be careful

  2. Fractals
    A fractal is a recursive graphic (that is, a graphic that appears the same at different levels as you zoom into it, so it appears to be made of smaller versions of itself). In theory you can zoom forever into a fractal, but here we will keep things simple and draw fractals only up to a specified level. Our base case will be when we reach that level. We'll use the following framework for most fractals:

    from cmu_112_graphics import * def appStarted(app): app.level = 1 def drawFractal(app, canvas, level, otherParams): if level == 0: pass # base case else: pass # recursive case; call drawFractal as needed with level-1 def keyPressed(app, event): if event.key in ['Up', 'Right']: app.level += 1 elif (event.key in ['Down', 'Left']) and (app.level > 0): app.level -= 1 def redrawAll(app, canvas): margin = min(app.width, app.height)//10 otherParams = None drawFractal(app, canvas, app.level, otherParams) canvas.create_text(app.width/2, 0, text = f'Level {app.level} Fractal', font = 'Arial ' + str(int(margin/3)) + ' bold', anchor='n') canvas.create_text(app.width/2, margin, text = 'Use arrows to change level', font = 'Arial ' + str(int(margin/4)), anchor='s') canvas.create_text(app.width/2, app.height/2, text = 'Replace this with your fractal', font = 'Arial 24 bold') runApp(width=400, height=400)

    1. sierpinskiTriangle
      from cmu_112_graphics import * def appStarted(app): app.level = 1 def drawSierpinskiTriangle(app, canvas, level, x, y, size): # (x,y) is the lower-left corner of the triangle # size is the length of a side # Need a bit of trig to calculate the top point if level == 0: topY = y - (size**2 - (size/2)**2)**0.5 canvas.create_polygon(x, y, x+size, y, x+size/2, topY, fill='black') else: # Bottom-left triangle drawSierpinskiTriangle(app, canvas, level-1, x, y, size/2) # Bottom-right triangle drawSierpinskiTriangle(app, canvas, level-1, x+size/2, y, size/2) # Top triangle midY = y - ((size/2)**2 - (size/4)**2)**0.5 drawSierpinskiTriangle(app, canvas, level-1, x+size/4, midY, size/2) def keyPressed(app, event): if event.key in ['Up', 'Right']: app.level += 1 elif (event.key in ['Down', 'Left']) and (app.level > 0): app.level -= 1 def redrawAll(app, canvas): margin = min(app.width, app.height)//10 x, y = margin, app.height-margin size = min(app.width, app.height) - 2*margin drawSierpinskiTriangle(app, canvas, app.level, x, y, size) canvas.create_text(app.width/2, 0, text = f'Level {app.level} Fractal', font = 'Arial ' + str(int(margin/3)) + ' bold', anchor='n') canvas.create_text(app.width/2, margin, text = 'Use arrows to change level', font = 'Arial ' + str(int(margin/4)), anchor='s') runApp(width=400, height=400)

      Result:


      Side-by-Side Levels:
      from cmu_112_graphics import * def appStarted(app): app.level = 1 def keyPressed(app, event): if event.key in ['Up', 'Right']: app.level += 1 elif (event.key in ['Down', 'Left']) and (app.level > 0): app.level -= 1 def drawSierpinskiTriangle(app, canvas, level, x, y, size): # (x,y) is the lower-left corner of the triangle # size is the length of a side # Need a bit of trig to calculate the top point if level == 0: topY = y - (size**2 - (size/2)**2)**0.5 canvas.create_polygon(x, y, x+size, y, x+size/2, topY, fill='black') else: # Bottom-left triangle drawSierpinskiTriangle(app, canvas, level-1, x, y, size/2) # Bottom-right triangle drawSierpinskiTriangle(app, canvas, level-1, x+size/2, y, size/2) # Top triangle midY = y - ((size/2)**2 - (size/4)**2)**0.5 drawSierpinskiTriangle(app, canvas, level-1, x+size/4, midY, size/2) # Add circles around app.level (left side) to show how # level N is made up of 3 level N-1's: if (level == app.level): h = size * 3**0.5/2 cx, cy, r = x+size/2, y-h/3, size/3**0.5 canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill=None, outline='pink') def drawLevel(app, canvas, level, cx): margin = min(app.width, app.height)//10 size = min(app.width, app.height) - 2*margin x, y = cx-size/2, app.height-margin drawSierpinskiTriangle(app, canvas, level, x, y, size) canvas.create_text(cx, 0, text = f'Level {level} Fractal', font = 'Arial ' + str(int(margin/3)) + ' bold', anchor='n') canvas.create_text(cx, margin, text = 'Use arrows to change level', font = 'Arial ' + str(int(margin/4)), anchor='s') def redrawAll(app, canvas): drawLevel(app, canvas, app.level, app.width/4) drawLevel(app, canvas, app.level+1, app.width*3/4) # draw the right arrow between the levels canvas.create_text(app.width/2, app.height/2, text=u'\u2192', font='Arial 50 bold') runApp(width=800, height=400)

    2. kochSnowflake
      # This example uses turtle graphics, not Tkinter import turtle def drawKochSide(length, level): if (level == 1): turtle.forward(length) else: drawKochSide(length/3, level-1) turtle.left(60) drawKochSide(length/3, level-1) turtle.right(120) drawKochSide(length/3, level-1) turtle.left(60) drawKochSide(length/3, level-1) def drawKochSnowflake(length, level): for step in range(3): drawKochSide(length, level) turtle.right(120) def drawKochExamples(): turtle.delay(1) turtle.speed(0) turtle.penup() turtle.goto(-300,100) turtle.pendown() turtle.pencolor('black') drawKochSide(300, 4) turtle.pencolor('blue') drawKochSnowflake(300, 4) turtle.penup() turtle.goto(-250,50) turtle.pendown() turtle.pencolor('red') drawKochSnowflake(200, 5) turtle.done() drawKochExamples()

      Result:



  3. Backtracking
    1. maze solving
      Python code: maze-solver.py
      Key excerpt:
      def solve(row,col): # base cases if (row,col) in visited: return False visited.add((row,col)) if (row,col)==(targetRow,targetCol): return True # recursive case for drow,dcol in [NORTH,SOUTH,EAST,WEST]: if isValid(data, row,col,(drow,dcol)): if solve(row+drow,col+dcol): return True visited.remove((row,col)) return False

    2. nQueens
      Python code: nQueens.py
      Key excerpt:
      def nQueensSolver(col, queenRow): # Recursive backtracker for nQueens that tries to insert a queen into column # col, where queenRow keeps track of the row # of the queen in each column if (col == n): return nQueensFormatSolution(queenRow) else: # try to place the queen in each row in turn in this col, # and then recursively solve the rest of the columns for row in range(n): if nQueensIsLegal(row, col, queenRow): queenRow[col] = row # place the queen and hope it works solution = nQueensSolver(col+1, queenRow) if (solution != None): # ta da! it did work return solution queenRow[col] = -1 # pick up the wrongly-placed queen # shoot, can't place the queen anywhere return None

  4. Memoization (optional)
    Memoization is a general technique to make certain recursive applications more efficient. The Big idea: when a program does a lot of repetitive computation, store results as they are computed, then look up and reuse results when available.

    1. The problem:
      def fib(n): if (n < 2): return 1 else: return fib(n-1) + fib(n-2) import time def testFib(maxN=40): for n in range(maxN+1): start = time.time() fibOfN = fib(n) ms = 1000*(time.time() - start) print(f'fib({n:2}) = {fibOfN:8}, time = {ms:5.2f}ms') testFib() # gets really slow!

    2. A solution:
      fibResults = dict() def fib(n): if (n in fibResults): return fibResults[n] if (n < 2): result = 1 else: result = fib(n-1) + fib(n-2) fibResults[n] = result return result import time def testFib(maxN=40): for n in range(maxN+1): start = time.time() fibOfN = fib(n) ms = 1000*(time.time() - start) print(f'fib({n:2}) = {fibOfN:8}, time = {ms:5.2f}ms') testFib() # ahhh, much better!

    3. A more elegant solution:
      def memoized(f): # You are not responsible for how this decorator works. You can just use it! import functools cachedResults = dict() @functools.wraps(f) def wrapper(*args): if args not in cachedResults: cachedResults[args] = f(*args) return cachedResults[args] return wrapper @memoized def fib(n): if (n < 2): return 1 else: return fib(n-1) + fib(n-2) import time def testFib(maxN=40): for n in range(maxN+1): start = time.time() fibOfN = fib(n) ms = 1000*(time.time() - start) print(f'fib({n:2}) = {fibOfN:8}, time = {ms:5.2f}ms') testFib() # ahhh, much better!