Lab 8: Midterm 2 Review (Optional)
Due by 11:59pm on Wednesday, April 2.
Starter Files
Download lab08.zip.
Attendance
Attendance is not required for this lab, since the midterm is on Tuesday 4/1.
Optional Questions
Getting Started Videos
These videos may provide some helpful direction for tackling the coding problems on this assignment.
To see these videos, you should be logged into your berkeley.edu email.
Mutable Trees
Consult this section if you need a refresher on Mutable Trees. It's
okay to skip directly to the questions and refer back here should you get stuck.
Mutable Trees
A Tree
instance has two instance attributes:
label
is the value stored at the root of the tree.branches
is a list ofTree
instances that hold the labels in the rest of the tree.
The Tree
class (with its __repr__
and __str__
methods omitted) is defined as:
class Tree:
"""A tree has a label and a list of branches.
>>> t = Tree(3, [Tree(2, [Tree(5)]), Tree(4)])
>>> t.label
3
>>> t.branches[0].label
2
>>> t.branches[1].is_leaf()
True
"""
def __init__(self, label, branches=[]):
self.label = label
for branch in branches:
assert isinstance(branch, Tree)
self.branches = list(branches)
def is_leaf(self):
return not self.branches
To construct a Tree
instance from a label x
(any value) and a list of branches bs
(a list of Tree
instances) and give it the name t
, write t = Tree(x, bs)
.
For a tree t
:
- Its root label can be any value, and
t.label
evaluates to it. - Its branches are always
Tree
instances, andt.branches
evaluates to the list of its branches. t.is_leaf()
returnsTrue
ift.branches
is empty andFalse
otherwise.- To construct a leaf with label
x
, writeTree(x)
.
Displaying a tree t
:
repr(t)
returns a Python expression that evaluates to an equivalent tree.str(t)
returns one line for each label indented once more than its parent with children below their parents.
>>> t = Tree(3, [Tree(1, [Tree(4), Tree(1)]), Tree(5, [Tree(9)])])
>>> t # displays the contents of repr(t)
Tree(3, [Tree(1, [Tree(4), Tree(1)]), Tree(5, [Tree(9)])])
>>> print(t) # displays the contents of str(t)
3
1
4
1
5
9
Changing (also known as mutating) a tree t
:
t.label = y
changes the root label oft
toy
(any value).t.branches = ns
changes the branches oft
tons
(a list ofTree
instances).- Mutation of
t.branches
will changet
. For example,t.branches.append(Tree(y))
will add a leaf labeledy
as the right-most branch. - Mutation of any branch in
t
will changet
. For example,t.branches[0].label = y
will change the root label of the left-most branch toy
.
>>> t.label = 3.0
>>> t.branches[1].label = 5.0
>>> t.branches.append(Tree(2, [Tree(6)]))
>>> print(t)
3.0
1
4
1
5.0
9
2
6
Here is a summary of the differences between the tree data abstraction implemented as a functional abstraction vs. implemented as a class:
- | Tree constructor and selector functions | Tree class |
---|---|---|
Constructing a tree | To construct a tree given a label and a list of branches , we call tree(label, branches) |
To construct a tree object given a label and a list of branches , we call Tree(label, branches) (which calls the Tree.__init__ method). |
Label and branches | To get the label or branches of a tree t , we call label(t) or branches(t) respectively |
To get the label or branches of a tree t , we access the instance attributes t.label or t.branches respectively. |
Mutability | The functional tree data abstraction is immutable (without violating its abstraction barrier) because we cannot assign values to call expressions | The label and branches attributes of a Tree instance can be reassigned, mutating the tree. |
Checking if a tree is a leaf | To check whether a tree t is a leaf, we call the function is_leaf(t) |
To check whether a tree t is a leaf, we call the method t.is_leaf() . This method can only be called on Tree objects. |
Visualizing Trees
If you would like some support with visualizing trees, please navigate
to code.cs61a.org, select Start Python Interpreter, and call autodraw()
.
Q1: Add Leaves
Implement add_d_leaves
, a function that takes in a Tree
instance t
and a number v
.
We define the depth of a node in t
to be the number of edges from the root to that node. The depth of root is therefore 0.
For each node in the tree, you should add d
leaves to it, where d
is the depth of the node. Every added leaf should have a label of v
. If the node at this depth has existing branches, you should add these leaves to the end of that list of branches.
For example, you should be adding 1 leaf with label v
to each node at depth 1, 2 leaves to each node at depth 2, and so on.
Here is an example of a tree t
(shown on the left) and the result after add_d_leaves
is applied with v
as 5.
Hint: Use a helper function to keep track of the depth!
Hint: Be careful about when you add the new leaves to the tree. We only want to add leaves to the original nodes in the tree, not to the nodes we added.
Take a look at the doctests below and try drawing out the second doctest to visualize how the function is mutating
t3
.
def add_d_leaves(t, v):
"""Add d leaves containing v to each node at every depth d.
>>> t_one_to_four = Tree(1, [Tree(2), Tree(3, [Tree(4)])])
>>> print(t_one_to_four)
1
2
3
4
>>> add_d_leaves(t_one_to_four, 5)
>>> print(t_one_to_four)
1
2
5
3
4
5
5
5
>>> t0 = Tree(9)
>>> add_d_leaves(t0, 4)
>>> t0
Tree(9)
>>> t1 = Tree(1, [Tree(3)])
>>> add_d_leaves(t1, 4)
>>> t1
Tree(1, [Tree(3, [Tree(4)])])
>>> t2 = Tree(2, [Tree(5), Tree(6)])
>>> t3 = Tree(3, [t1, Tree(0), t2])
>>> print(t3)
3
1
3
4
0
2
5
6
>>> add_d_leaves(t3, 10)
>>> print(t3)
3
1
3
4
10
10
10
10
10
10
0
10
2
5
10
10
6
10
10
10
"""
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q add_d_leaves
Q2: Has Path
Write a function has_path
that takes in a Tree t
and a string target
. It
returns True
if there is a path that starts from the root where the entries
along the path spell out the target
, and False
otherwise. You may assume
that every node's label
is exactly one character.
This data structure is called a trie, and it has a lot of cool applications, such as autocomplete.
def has_path(t, target):
"""Return whether there is a path in a Tree where the entries along the path
spell out a particular target.
>>> greetings = Tree('h', [Tree('i'),
... Tree('e', [Tree('l', [Tree('l', [Tree('o')])]),
... Tree('y')])])
>>> print(greetings)
h
i
e
l
l
o
y
>>> has_path(greetings, 'h')
True
>>> has_path(greetings, 'i')
False
>>> has_path(greetings, 'hi')
True
>>> has_path(greetings, 'hello')
True
>>> has_path(greetings, 'hey')
True
>>> has_path(greetings, 'bye')
False
>>> has_path(greetings, 'hint')
False
"""
assert len(target) > 0, 'no path for empty target.'
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q has_path
Q3: Level Mutation Link
As a reminder, the depth of a node is defined as the number of edges from the root to the node. Therefore, the root itself has a depth of 0 since there are no edges between the root and itself.
Given a tree t
and a linked list of one-argument functions funcs
, write a function that mutates the labels of t
using the functions from funcs
at the corresponding depths. For example:
- The label at the root node (depth 0) will be mutated using the function at depth 0 of
funcs
(funcs.first
). - The label at the first level of the tree will be mutated using the function at depth 1 of
funcs
(funcs.rest.first
), and so on.
Each function in funcs
takes in a label value and returns a valid label value.
If a node is a leaf and there are remaining functions in funcs
, all of these remaining functions should be applied in order to the label of the leaf node. If funcs
is empty, the tree should remain unmodified.
Please refer to the doctests for examples.
def level_mutation_link(t, funcs):
"""Mutates t using the functions in the linked list funcs.
>>> t = Tree(1, [Tree(2, [Tree(3)])])
>>> funcs = Link(lambda x: x + 1, Link(lambda y: y * 5, Link(lambda z: z ** 2)))
>>> level_mutation_link(t, funcs)
>>> t # At level 0, apply x + 1; at level 1, apply y * 5; at level 2 (leaf), apply z ** 2
Tree(2, [Tree(10, [Tree(9)])])
>>> t2 = Tree(1, [Tree(2), Tree(3, [Tree(4)])])
>>> level_mutation_link(t2, funcs)
>>> t2 # Level 0: 1+1=2; Level 1: 2*5=10 => 10**2 = 100, 3*5=15; Level 2 (leaf): 4**2=16
Tree(2, [Tree(100), Tree(15, [Tree(16)])])
>>> t3 = Tree(1, [Tree(2)])
>>> level_mutation_link(t3, funcs)
>>> t3 # Level 0: 1+1=2; Level 1: 2*5=10; no further levels, so apply remaining z ** 2: 10**2=100
Tree(2, [Tree(100)])
"""
if _____________________:
return
t.label = _____________________
remaining = _____________________
if __________________ and __________________:
while _____________________:
_____________________
remaining = remaining.rest
for b in t.branches:
_____________________
Use Ok to test your code:
python3 ok -q level_mutation_link
Recursion and Tree Recursion
Q4: Merge Numbers
Write a procedure merge(n1, n2)
, which takes numbers with digits in decreasing
order and returns a single number with all of the digits of the two
in decreasing order. Any number merged with 0
will be that number
(treat 0
as having no digits). Use recursion.
def merge_numbers(n1, n2):
"""Merges two numbers that have decreasing digits.
>>> merge_numbers(31, 42)
4321
>>> merge_numbers(21, 0)
21
>>> merge_numbers(21, 31)
3211
"""
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q merge_numbers
Q5: Subsequences
A subsequence of a sequence s
is a subset of elements from s
, in the same
order they appear in s
. Consider the list [1, 2, 3]
. A few of its
subsequences are []
, [1, 3]
, [2]
, and [1, 2, 3]
.
Write a function that takes in a list and returns all possible subsequences of that list. The subsequences should be returned as a list of lists, where each nested list is a subsequence of the original input.
In order to accomplish this, you might first want to write a function insert_into_all
that takes an item and a list of lists, adds the item to the beginning of each nested list,
and returns the resulting list.
def insert_into_all(item, nested_list):
"""Return a new list consisting of all the lists in nested_list,
but with item added to the front of each. You can assume that
nested_list is a list of lists.
>>> nl = [[], [1, 2], [3]]
>>> insert_into_all(0, nl)
[[0], [0, 1, 2], [0, 3]]
"""
"*** YOUR CODE HERE ***"
def subseqs(s):
"""Return a nested list (a list of lists) of all subsequences of S.
The subsequences can appear in any order. You can assume S is a list.
>>> seqs = subseqs([1, 2, 3])
>>> sorted(seqs)
[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
>>> subseqs([])
[[]]
"""
if ________________:
________________
else:
________________
________________
Use Ok to test your code:
python3 ok -q subseqs
Q6: Non-Decreasing Subsequences
Just like the previous question, we want to write a function that takes a list and returns a list of lists, where each individual list is a subsequence of the original input.
However, this time we have another condition: we only want the subsequences for which
consecutive elements are nondecreasing. For example, [1, 3, 2]
is a
subsequence of [1, 3, 2, 4]
, but since 2
< 3
, this subsequence would not
be included in our result.
You may assume that the list passed in as s
contains only nonnegative elements.
You may use the insert_into_all
helper function from the previous part.
def non_decrease_subseqs(s):
"""Return a nested list of all subsequences of S (a list of lists)
for which the elements of the subsequence are nondecreasing. The
subsequences can appear in any order. You can assume S is a list.
>>> seqs = non_decrease_subseqs([1, 3, 2])
>>> sorted(seqs)
[[], [1], [1, 2], [1, 3], [2], [3]]
>>> non_decrease_subseqs([])
[[]]
>>> seqs2 = non_decrease_subseqs([1, 1, 2])
>>> sorted(seqs2)
[[], [1], [1], [1, 1], [1, 1, 2], [1, 2], [1, 2], [2]]
"""
def subseq_helper(s, prev):
if not s:
return ____________________
elif s[0] < prev:
return ____________________
else:
a = ______________________
b = ______________________
return insert_into_all(________, ______________) + ________________
return subseq_helper(____, ____)
Use Ok to test your code:
python3 ok -q non_decrease_subseqs
Generators
Q7: Generate Permutations
Given a sequence of unique elements, a permutation of the sequence is a list
containing the elements of the sequence in some arbitrary order. For example,
[1, 2, 3]
, [2, 1, 3]
, [1, 3, 2]
, and [3, 2, 1]
are some of the
permutations of the sequence [1, 2, 3]
.
Implement perms
, a generator function that takes in a sequence seq
and returns a generator that yields all permutations of seq
. For this question,
assume that seq
will not be empty.
Permutations may be yielded in any order.
Hint: Remember, it's possible to loop over generator objects because generators are iterators!
def perms(seq):
"""Generates all permutations of the given sequence. Each permutation is a
list of the elements in SEQ in a different order. The permutations may be
yielded in any order.
>>> p = perms([100])
>>> type(p)
<class 'generator'>
>>> next(p)
[100]
>>> try: # Prints "No more permutations!" if calling next would cause an error
... next(p)
... except StopIteration:
... print('No more permutations!')
No more permutations!
>>> sorted(perms([1, 2, 3])) # Returns a sorted list containing elements of the generator
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
>>> sorted(perms((10, 20, 30)))
[[10, 20, 30], [10, 30, 20], [20, 10, 30], [20, 30, 10], [30, 10, 20], [30, 20, 10]]
>>> sorted(perms("ab"))
[['a', 'b'], ['b', 'a']]
"""
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q perms
Mutability
Q8: Shuffle Pairs
Implement a function shuffle_pairs(lst)
that takes an even-length list and mutates it by swapping adjacent pairs. For instance, if we have the list [1, 2, 3, 4]
, then we should swap 1
with 2
and 3
with 4
.
def shuffle_pairs(lst):
"""Swap adjacent pairs of elements in place.
>>> nums = list(range(6))
>>> shuffle_pairs(nums)
>>> nums
[1, 0, 3, 2, 5, 4]
>>> letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
>>> shuffle_pairs(letters)
>>> letters
['b', 'a', 'd', 'c', 'f', 'e', 'h', 'g']
>>> multiples = [10, 20, 30, 40]
>>> shuffle_pairs(multiples)
>>> multiples
[20, 10, 40, 30]
>>> pair = [1, 2]
>>> shuffle_pairs(pair)
>>> pair
[2, 1]
>>> empty = []
>>> shuffle_pairs(empty)
>>> empty
[]
"""
assert len(lst) % 2 == 0, 'len(lst) must be even'
half = ________________
for ____ in ____:
_____, _____ = _____, _____
Use Ok to test your code:
python3 ok -q shuffle_pairs
Q9: Common Players
Implement the function common_players
. The common_players
function takes in a roster
dictionary that
maps players to their teams, and returns a new dictionary that maps teams to a list of players on that team.
The order of player names in the list does not matter.
def common_players(roster):
"""Returns a dictionary containing values along with a corresponding
list of keys that had that value from the original dictionary.
>>> full_roster = {
... "bob": "Team A",
... "barnum": "Team B",
... "beatrice": "Team C",
... "bernice": "Team B",
... "ben": "Team D",
... "belle": "Team A",
... "bill": "Team B",
... "bernie": "Team B",
... "baxter": "Team A"
... }
>>> player_dict = common_players(full_roster)
>>> type(player_dict) == dict
True
>>> for key, val in sorted(player_dict.items()):
... print(key, list(sorted(val)))
Team A ['baxter', 'belle', 'bob']
Team B ['barnum', 'bernice', 'bernie', 'bill']
Team C ['beatrice']
Team D ['ben']
"""
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q common_players
Object-Oriented Programming
Election
Let's implement a game called Election. In this game, two players compete to try and earn the most votes. Both players start with 0 votes and 100 popularity.
The two players alternate turns, and the first player starts. Each turn, the current player chooses an action. There are two types of actions:
- The player can debate, and either gain or lose 50 popularity. If the player
has popularity
p1
and the other player has popularityp2
, then the probability that the player gains 50 popularity ismax(0.1, p1 / (p1 + p2))
. Note that themax
here ensures that the probability is never lower than 0.1. - The player can give a speech. If the player has popularity
p1
and the other player has popularityp2
, then the player gainsp1 // 10
votes and popularity and the other player losesp2 // 10
popularity.
The game ends when a player reaches 50 votes, or after a total of 10 turns have been played (each player has taken 5 turns). Whoever has more votes at the end of the game is the winner!
Q10: Player
First, let's implement the Player
class. Fill in the debate
and speech
methods, that take in another Player
other
, and implement the correct
behavior as detailed above. Here are a few additional things to keep in mind:
- Each player carries a random number generator (the
random_func
instance attribute), which is a function that returns a random float between 0 and 1 when called. - In the
debate
method, you should call therandom_func
function to get a random number. The player should gain 50 popularity if the random number is smaller than the probability described above, or lose 50 popularity otherwise. - Neither players' popularity should ever become negative. If this happens, set it equal to 0 instead.
### Phase 1: The Player Class
class Player:
"""
>>> random = make_test_random()
>>> p1 = Player('Hill', random)
>>> p2 = Player('Don', random)
>>> p1.popularity
100
>>> p1.debate(p2) # random() should return 0.0
>>> p1.popularity
150
>>> p2.popularity
100
>>> p2.votes
0
>>> p2.speech(p1)
>>> p2.votes
10
>>> p2.popularity
110
>>> p1.popularity
135
>>> p1.speech(p2)
>>> p1.votes
13
>>> p1.popularity
148
>>> p2.votes
10
>>> p2.popularity
99
>>> for _ in range(4): # 0.1, 0.2, 0.3, 0.4
... p1.debate(p2)
>>> p2.debate(p1)
>>> p2.popularity
49
>>> p2.debate(p1)
>>> p2.popularity
0
"""
def __init__(self, name, random_func):
self.name = name
self.votes = 0
self.popularity = 100
self.random_func = random_func
def debate(self, other):
"*** YOUR CODE HERE ***"
def speech(self, other):
"*** YOUR CODE HERE ***"
def choose(self, other):
return self.speech
Use Ok to test your code:
python3 ok -q Player
Q11: Game
Now, implement the Game
class. Fill in the play
method, which should
alternate between the two players, starting with p1
, and have each player take
one turn at a time. The choose
method in the Player
class returns the
method, either debate
or speech
, that should be called to perform the
action.
In addition, fill in the winner
method, which should return the
player with more votes, or None
if the players are tied.
### Phase 2: The Game Class
class Game:
"""
>>> random = make_test_random()
>>> p1, p2 = Player('Hill',random), Player('Don', random)
>>> g = Game(p1, p2)
>>> winner = g.play()
>>> p1 is winner
True
>>> # Additional correctness tests
>>> winner is g.winner()
True
>>> g.turn
10
>>> p1.votes = p2.votes
>>> print(g.winner())
None
"""
def __init__(self, player1, player2):
self.p1 = player1
self.p2 = player2
self.turn = 0
def play(self):
while not self.game_over():
"*** YOUR CODE HERE ***"
return self.winner()
def game_over(self):
return max(self.p1.votes, self.p2.votes) >= 50 or self.turn >= 10
def winner(self):
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q Game
Q12: New Players
The choose
method in the Player
class is boring because it always returns
the speech
method. Let's implement two new classes that inherit from Player
,
but have more interesting choose
methods.
Implement the choose
method in the AggressivePlayer
class, which returns the
debate
method if the player's popularity is less than or equal to other
's
popularity, and speech
otherwise. Also implement the choose
method in the
CautiousPlayer
class, which returns the debate
method if the player's
popularity is 0, and speech
otherwise.
### Phase 3: New Players
class AggressivePlayer(Player):
"""
>>> random = make_test_random()
>>> p1, p2 = AggressivePlayer('Don', random), Player('Hill', random)
>>> g = Game(p1, p2)
>>> winner = g.play()
>>> p1 is winner
True
>>> # Additional correctness tests
>>> p1.popularity = p2.popularity
>>> p1.choose(p2) == p1.debate
True
>>> p1.popularity += 1
>>> p1.choose(p2) == p1.debate
False
>>> p2.choose(p1) == p2.speech
True
"""
def choose(self, other):
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q AggressivePlayer
class CautiousPlayer(Player):
"""
>>> random = make_test_random()
>>> p1, p2 = CautiousPlayer('Hill', random), AggressivePlayer('Don', random)
>>> p1.popularity = 0
>>> p1.choose(p2) == p1.debate
True
>>> p1.popularity = 1
>>> p1.choose(p2) == p1.debate
False
>>> # Additional correctness tests
>>> p2.choose(p1) == p2.speech
True
"""
def choose(self, other):
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q CautiousPlayer
Linked Lists
Visualizing Linked Lists
If you would like some support with visualizing linked lists, please navigate
to code.cs61a.org, select Start Python Interpreter,
and call autodraw()
.
Q13: Every Other
Implement every_other
, which takes a linked list s
. It mutates s
such
that all of the odd-indexed elements (using 0-based indexing) are removed from
the list. For example:
>>> s = Link('a', Link('b', Link('c', Link('d'))))
>>> every_other(s)
>>> s.first
'a'
>>> s.rest.first
'c'
>>> s.rest.rest is Link.empty
True
If s
contains fewer than two elements, s
remains unchanged.
def every_other(s):
"""Mutates a linked list so that all the odd-indiced elements are removed
(using 0-based indexing).
>>> s = Link(1, Link(2, Link(3, Link(4))))
>>> every_other(s)
>>> s
Link(1, Link(3))
>>> odd_length = Link(5, Link(3, Link(1)))
>>> every_other(odd_length)
>>> odd_length
Link(5, Link(1))
>>> singleton = Link(4)
>>> every_other(singleton)
>>> singleton
Link(4)
"""
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q every_other
Q14: Slice
Implement a function slice_link
that slices a given linked list link
. slice_link
should slice the link
starting at start
and ending one element before
end
, as with a normal Python list. Additionally, similar to slicing normal
Python lists, this function should return a new list and not modify the original list.
def slice_link(link, start, end):
"""Slices a linked list 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(new)
<1 4 1>
"""
"*** YOUR CODE HERE ***"
Use Ok to test your code:
python3 ok -q slice_link