Diagnostic 5: Week 5 review
Mutable Linked Lists
Question 1: Slice
Implement a function slice_link
that slices a given link
. slice_link
should slice the link
starting at start
and ending one element before
end
, as with a normal Python list.
def slice_link(link, start, end):
"""Slices a Link from start to end (as with a normal Python list).
>>> link = Link(3, Link(1, Link(4, Link(1, Link(5, Link(9))))))
>>> new = slice_link(link, 1, 4)
>>> print_link(new)
<1 4 1>
"""
"*** YOUR CODE HERE ***"
if end == 0:
return Link.empty
elif start == 0:
return Link(link.first, slice_link(link.rest, 0, end-1))
else:
return slice_link(link.rest, start-1, end-1)
Note: This was an extra question in Lab 9.
Question 2: Add Links
Let's implement a method in order to add together items of link1
and link2
.
Do not assume that the links are the same length.
def add_links(link1, link2):
"""Adds two Links, returning a new Link
>>> l1 = Link(1, Link(2))
>>> l2 = Link(3, Link(4, Link(5)))
>>> new = add_links(l1,l2)
>>> print_link(new)
<1 2 3 4 5>
"""
"*** YOUR CODE HERE ***"
if link1 is not Link.empty:
return Link(link1.first, add_links(link1.rest, link2))
elif link2 is not Link.empty:
return Link(link2.first, add_links(link1, link2.rest))
else:
return Link.empty
# Iterative version (using reverse)
def add_links(link1, link2):
result = Link.empty
while link1 is not Link.empty:
result = Link(link1.first, result)
link1 = link1.rest
while link2 is not Link.empty:
result = Link(link2.first, result)
link2 = link2.rest
return reverse(result)
Note: This was an extra question in Lab 9.
Implement mutate_reverse
, a recursive mutating version of reverse
. Have it
mutate the original Link
so that its elements are reversed.
def mutate_reverse(link):
"""Mutates the Link so that its elements are reversed.
>>> link = Link(1)
>>> mutate_reverse(link)
>>> link
Link(1)
>>> link = Link(1, Link(2, Link(3)))
>>> mutate_reverse(link)
>>> link
Link(3, Link(2, Link(1)))
"""
"*** YOUR CODE HERE ***"
if link is Link.empty or link.rest is Link.empty:
return
mutate_reverse(link.rest)
while link.rest is not Link.empty:
link.first, link.rest.first = link.rest.first, link.first
link = link.rest
# O(n) solution
def mutate_reverse(link):
"""Mutates the Link so that its elements are reversed.
>>> link = Link(1)
>>> mutate_reverse(link)
>>> link
Link(1)
>>> link = Link(1, Link(2, Link(3)))
>>> mutate_reverse(link)
>>> link
Link(3, Link(2, Link(1)))
"""
reverse_helper(link)
def reverse_helper(link):
if link.rest is not Link.empty:
second, last = link.rest, link
link = reverse_helper(second)
second.rest, last.rest = last, link.empty
return link
Mutable Trees
Question 3: Cumulative sum
Write a function cumulative_sum
that returns a new Tree
, where each entry is the sum of all entries in the corresponding subtree of the old Tree
.
def cumulative_sum(t):
"""Return a new Tree, where each entry is the sum of all entries in the
corresponding subtree of t.
>>> t = Tree(1, [Tree(3, [Tree(5)]), Tree(7)])
>>> cumulative = cumulative_sum(t)
>>> t
Tree(1, [Tree(3, [Tree(5)]), Tree(7)])
>>> cumulative
Tree(16, [Tree(8, [Tree(5)]), Tree(7)])
>>> cumulative_sum(Tree(1))
Tree(1)
"""
"*** YOUR CODE HERE ***"
subtrees = [cumulative_sum(st) for st in t.subtrees]
new_entry = sum(st.entry for st in subtrees) + t.entry
return Tree(new_entry, subtrees)
Question 4: Leaves
Write a function leaves
that returns a list of all the entries of the leaf
nodes of a Tree
.
def leaves(t):
"""Returns a list of all the entries of the leaf nodes of the Tree t.
>>> leaves(Tree(1))
[1]
>>> leaves(Tree(1, [Tree(2, [Tree(3)]), Tree(4)]))
[3, 4]
"""
"*** YOUR CODE HERE ***"
if t.is_leaf():
return [t.entry]
all_leaves = []
for b in t.subtrees:
all_leaves += leaves(b)
return all_leaves
Binary Trees and Binary Search Trees
Question 5: Size
Define the function size
which takes in a BinaryTree
as an argument and
returns the number of entries in the tree.
def size(b):
r""" Return the number of entries in the binary tree b.
>>> b = BinaryTree(4,
... BinaryTree(2,
... BinaryTree(1)),
... BinaryTree(6,
... BinaryTree(5)))
>>> size(b)
5
"""
"*** YOUR CODE HERE ***"
if b is BinaryTree.empty:
return 0
return 1 + size(tree.left) + size(tree.right)
Question 6: Max
Define the function bst_max
which takes in a binary search tree and returns
the max of all of the values of each node in the tree. Be sure to take
advantage of the properties of the binary search tree to get an efficient
solution.
You may assume the input is a non-empty BinaryTree
.
def bst_max(b):
r""" Returns the max of all the values of each node in b.
Calling bst_max on the following tree should return 6:
4
/ \
2 6
/ /
1 5
>>> b = BinaryTree(4,
... BinaryTree(2,
... BinaryTree(1)),
... BinaryTree(6,
... BinaryTree(5)))
>>> bst_max(t)
6
"""
"*** YOUR CODE HERE ***"
if b.right is BinaryTree.empty:
return b.entry
return bst_max(b.right)
Question 7: Min
Define the function bst_min
which takes in a binary search tree and returns
the min of all of the values of each node in the tree. Be sure to take
advantage of the properties of the binary search tree to get an efficient
solution.
You may assume the input is a non-empty BinaryTree
.
def bst_min(b):
r""" Returns the min of all the values of each node in b.
Calling bst_min on the following tree should return 1:
4
/ \
2 6
/ /
1 5
>>> b = BinaryTree(4,
... BinaryTree(2,
... BinaryTree(1)),
... BinaryTree(6,
... BinaryTree(5)))
>>> bst_min(t)
1
"""
"*** YOUR CODE HERE ***"
if b.left is BinaryTree.empty:
return b.entry
return bst_min(b.left)
Interfaces
For the following interface questions, you'll be implementing magic methods for
the following Rational
class, which represents a rational number. A rational
number consists of a numerator and a denominator, both of which are integers.
from fractions import gcd
class Rational:
def __init__(self, numer, denom):
factor = gcd(numer, denom)
self.numer = numer // factor
self.denom = denom // factor
Question 8: Repr
Implement the __repr__
method for the Rational
class. See the
doctests for expected behavior.
def __repr__(self):
"""
>>> r = Rational(4, 6)
>>> r
Rational(2, 3)
>>> repr(r)
'Rational(2, 3)'
"""
"*** YOUR CODE HERE ***"
return 'Rational({0}, {1})'.format(self.numer, self.denom)
Question 9: Str
Implement the __str__
method for the Rational
class. See the
doctests for expected behavior.
def __str__(self):
"""
>>> r = Rational(4, 6)
>>> print(r)
2 / 3
>>> str(r)
'2 / 3'
"""
"*** YOUR CODE HERE ***"
return '{0} / {1}'.format(self.numer, self.denom)
Question 10: Add
Implement the __add__
method for the Rational
class. See the
doctests for expected behavior.
def __add__(self, other):
"""
>>> r1 = Rational(2, 3)
>>> r2 = Rational(4, 5)
>>> r1 + r2
Rational(22, 15)
>>> r1 + 6
Rational(20, 3)
"""
"*** YOUR CODE HERE ***"
if isinstance(other, Rational):
return Rational(self.numer * other.denom + self.denom * other.numer,
self.denom * other.denom)
elif isinstance(other, int):
return Rational(self.numer + self.denom * other, self.denom)
Question 11: Neg
Implement the __neg__
method for the Rational
class. See the
doctests for expected behavior.
def __neg__(self):
"""
>>> r = Rational(2, 3)
>>> -r
Rational(-2, 3)
"""
"*** YOUR CODE HERE ***"
return Rational(-self.numer, self.denom)
Question 12: Sub
Implement the __sub__
method for the Rational
class. See the
doctests for expected behavior.
def __sub__(self, other):
"""
>>> r1 = Rational(2, 3)
>>> r2 = Rational(4, 5)
>>> r1 - r2
Rational(-2, 15)
>>> r1 - 1
Rational(-1, 3)
"""
"*** YOUR CODE HERE ***"
return self + (-other)
Question 13: Mul
Implement the __mul__
method for the Rational
class. See the
doctests for expected behavior.
def __mul__(self, other):
"""
>>> r1 = Rational(2, 3)
>>> r2 = Rational(9, 5)
>>> r1 * r2
Rational(6, 5)
>>> r1 * 5
Rational(10, 3)
"""
"*** YOUR CODE HERE ***"
if isinstance(other, Rational):
return Rational(self.numer * other.numer, self.denom * other.denom)
elif isinstance(other, int):
return Rational(self.numer * other, self.denom)
Iterators and Generators
Question 14: Does it work?
Consider the following iterators. Which ones work and which ones don't? Why?
class IteratorA:
def __init__(self):
self.start = 5
def __next__(self):
if self.start == 100:
raise StopIteration
self.start += 5
return self.start
def __iter__(self):
return self
No problem, this is a beautiful iterator.
class IteratorB:
def __init__(self):
self.start = 5
def __iter__(self):
return self
Oh no! Where is __next__
? This fails to implement the iterator
interface because calling __iter__
doesn't return something that has
a __next__
method.
class IteratorC:
def __init__(self):
self.start = 5
def __next__(self):
if self.start == 10:
raise StopIteration
self.start += 1
return self.start
This also fails to implement the iterator interface. Without the
__iter__
method, the for
loop will error. The for
loop needs to
call __iter__
first because some objects might not implement the
__next__
method themselves, but calling __iter__
will return an
object that does.
class IteratorD:
def __init__(self):
self.start = 1
def __next__(self):
self.start += 1
return self.start
def __iter__(self):
return self
This is technically an iterator, because it implements both __iter__
and
__next__
. Notice that it's an infinite sequence! Sequences like these are
the reason iterators are useful. Because iterators delay computation, we can
use a finite amount of memory to represent an infinitely long sequence.
Question 15: Str
Write an iterator that takes a string as input:
class Str:
def __init__(self, str):
self.str = str
def __iter__(self):
return self.str.__iter__()
That works (why?), but just kidding.
class Str:
"""
>>> s = Str("hello")
>>> for char in s:
... print(char)
...
h
e
l
l
o
"""
def __init__(self, str):
"*** YOUR CODE HERE ***"
self.str = str
self.i = -1
def __iter__(self):
"*** YOUR CODE HERE ***"
return self
def __next__(self):
self.i += 1
if self.i >= len(self.str):
raise StopIteration
return self.str[self.i]
Question 16: Countdown
Write a generator that counts down to 0.
Write it in both ways: using a generator function on its own, and
within the __iter__
method of a class.
def countdown(n):
"""
>>> for number in countdown(5):
... print(number)
...
5
4
3
2
1
0
"""
"*** YOUR CODE HERE ***"
while n >= 0:
yield n
n = n - 1
class Countdown:
"""
>>> for number in Countdown(5):
... print(number)
...
5
4
3
2
1
0
"""
"*** YOUR CODE HERE ***"
def __init__(self, cur):
self.cur = cur
def __iter__(self):
return self
def __next__(self):
result = self.cur
if result < 0:
raise StopIteration
self.cur -= 1
return result
# Alternate solution that makes __iter__ a generator function:
class Countdown:
def __init__(self, cur):
self.cur = cur
def __iter__(self):
while self.cur >= 0:
yield self.cur
self.cur -= 1
Question 17: Hailstone
Write a generator that outputs the hailstone sequence from homework 1.
def hailstone(n):
"""
>>> for num in hailstone(10):
... print(num)
...
10
5
16
8
4
2
1
"""
"*** YOUR CODE HERE ***"
i = n
while i > 1:
yield i
if i % 2 == 0:
i //= 2
else:
i = i * 3 + 1
yield i
Question 18: Pairs (generator)
Write a generator function pairs
that takes a list and yields all the
possible pairs of elements from that list.
Note that this means that you should be yielding a tuple.
def pairs(lst):
"""
>>> type(pairs([3, 4, 5]))
<class 'generator'>
>>> for x, y in pairs([3, 4, 5]):
... print(x, y)
...
3 3
3 4
3 5
4 3
4 4
4 5
5 3
5 4
5 5
"""
"*** YOUR CODE HERE ***"
for i in lst:
for j in lst:
yield i, j
Question 19: Pairs (iterator)
Now write an iterator that does the same thing. You are only allowed to use a linear amount of space - so computing a list of all of the possible pairs is not a valid answer. Notice how much harder it is - this is why generators are useful.
class PairsIterator:
"""
>>> for x, y in PairsIterator([3, 4, 5]):
... print(x, y)
...
3 3
3 4
3 5
4 3
4 4
4 5
5 3
5 4
5 5
"""
def __init__(self, lst):
"*** YOUR CODE HERE ***"
self.lst = lst
self.i = 0
self.j = 0
def __next__(self):
"*** YOUR CODE HERE ***"
if self.i == len(self.lst):
raise StopIteration
result = (self.lst[self.i], self.lst[self.j])
if self.j == len(self.lst) - 1:
self.i += 1
self.j = 0
else:
self.j += 1
return result
def __iter__(self):
"*** YOUR CODE HERE ***"
return self