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.
printFiles
import os
defprintFiles(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.
listFiles
import os
deflistFiles(path):if os.path.isfile(path):
# Base Case: return a list of just this filereturn [ 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'))
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
defremoveTempFiles(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
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 *
defappStarted(app):
app.level = 1defdrawFractal(app, canvas, level, otherParams):if level == 0:
pass# base caseelse:
pass# recursive case; call drawFractal as needed with level-1defkeyPressed(app, event):if event.key in ['Up', 'Right']:
app.level += 1elif (event.key in ['Down', 'Left']) and (app.level > 0):
app.level -= 1defredrawAll(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)
sierpinskiTriangle
from cmu_112_graphics import *
defappStarted(app):
app.level = 1defdrawSierpinskiTriangle(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 pointif 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)
defkeyPressed(app, event):if event.key in ['Up', 'Right']:
app.level += 1elif (event.key in ['Down', 'Left']) and (app.level > 0):
app.level -= 1defredrawAll(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 *
defappStarted(app):
app.level = 1defkeyPressed(app, event):if event.key in ['Up', 'Right']:
app.level += 1elif (event.key in ['Down', 'Left']) and (app.level > 0):
app.level -= 1defdrawSierpinskiTriangle(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 pointif 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')
defdrawLevel(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')
defredrawAll(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)
Koch curve and Koch snowflake
The Koch curve is a fractal that replaces a line segment with 4 connected line segments that are all 1/3 of the size of the original line.
from cmu_112_graphics import *
import math
defnewKochCurvePoints(start, end):# Return three new points that will form the four new lines# Lines:# start -> point1 (first third or orginal)# point1 -> point2# point2 -> point3# point3 -> end (last third or orginal)# Point1# One third of the way from start to end
point1x = (end[0]-start[0])*1/3 + start[0]
point1y = (end[1]-start[1])*1/3 + start[1]
point1 = (point1x, point1y)
# Point3# Two thirds of the way from start to end
point3x = (end[0]-start[0])*2/3 + start[0]
point3y = (end[1]-start[1])*2/3 + start[1]
point3 = (point3x, point3y)
# Point2# Start with halfway between start and end
point2x = (end[0]-start[0])*1/2 + start[0]
point2y = (end[1]-start[1])*1/2 + start[1]
# perpendicular change, scaled appropriately
dy = -(end[0]-start[0])
dx = (end[1]-start[1])
scale = math.sqrt((1/3)**2 - (1/6)**2)/1
point2x += scale*dx
point2y += scale*dy
point2 = (point2x, point2y)
return (point1, point2, point3)
defappStarted(app):
app.level = 0defdrawFractal(app, canvas, level, start, end):
dist = math.sqrt((end[0]-start[0])**2 + (end[1]-start[1])**2)
if level == 0or dist <= 1:
canvas.create_line(start[0], start[1], end[0], end[1])
else:
point1, point2, point3 = newKochCurvePoints(start, end)
drawFractal(app, canvas, level-1, start, point1)
drawFractal(app, canvas, level-1, point1, point2)
drawFractal(app, canvas, level-1, point2, point3)
drawFractal(app, canvas, level-1, point3, end)
defkeyPressed(app, event):if event.key in ['Up', 'Right']:
app.level += 1elif (event.key in ['Down', 'Left']) and (app.level > 0):
app.level -= 1defredrawAll(app, canvas):
margin = min(app.width, app.height)//10
start = (margin, app.height-margin*2)
end = (app.width-margin, app.height-margin*2)
drawFractal(app, canvas, app.level, start, end)
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=800, height=400)
defnQueens(n):# Set up an empty board and call our recursive solver
board = [['_']*n for row in range(n)]
return nQueensSolver(board, 0)
defnQueensSolver(board, newQueenRow):
rows, cols = len(board), len(board[0])
if newQueenRow >= rows: # If we are trying to place beyond the board...return board # ...we're all done! Return the solution.else:
# For each column in newQueenRow...for newQueenCol in range(cols):
if isLegalMove(board, newQueenRow, newQueenCol):
# Make the move if it is legal.
board[newQueenRow][newQueenCol] = 'Q'# Recursively try to solve from this point
result = nQueensSolver(board, newQueenRow + 1)
if result != None: # Return the solution if we found onereturn result
# Undo the move if we did not find a solution
board[newQueenRow][newQueenCol] = '_'returnNone# Return None if no moves lead to a solution
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.
The problem:
deffib(n):if (n < 2):
return1else:
return fib(n-1) + fib(n-2)
import time
deftestFib(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!
A solution:
fibResults = dict()
deffib(n):if (n in fibResults):
return fibResults[n]
if (n < 2):
result = 1else:
result = fib(n-1) + fib(n-2)
fibResults[n] = result
return result
import time
deftestFib(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!
A more elegant solution:
defmemoized(f):# You are not responsible for how this decorator works. You can just use it!import functools
cachedResults = dict()
@functools.wraps(f)defwrapper(*args):if args notin cachedResults:
cachedResults[args] = f(*args)
return cachedResults[args]
return wrapper
@memoizeddeffib(n):if (n < 2):
return1else:
return fib(n-1) + fib(n-2)
import time
deftestFib(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!