提示:本文内容来源于UCB CS61A 2020 Summer课程,详情请点击CS 61A: Structure and Interpretation of Computer Programs
前言
本文为Lab 5: Python Lists, Trees内容
提示:以下是本篇文章正文内容,叙述部分参考题目描述,由本人与chatgpt共同翻译,思路部分及作业部分为作者完成,仅供参考,若有错误欢迎评论指正
Lab 5: Python Lists, Trees
Topics 实验主题
List Comprehensions 列表推导式
列表推导式是一种紧凑而强大的创建新列表的方式,其语法如下:
[<expression> for <element> in <sequence> if <conditional>]
以上推导式可以读作:对于<sequence>中的每一个元素,如果<conditional>的值为真,那么计算其<expression>的值,这些计算结果共同组成新列表
范例如下:
>>> [i**2 for i in [1, 2, 4, 4] if i % 2 == 0]
[4, 16]
其中,对于[1, 2, 3, 4]
中的每一个元素i
,若满足i % 2 == 0
,则我们计算i**2
的值,并且将计算结果插入到新列表中
也可以成为,列表推导式将会创建一个新列表,该新列表包含原列表中每个偶数的平方
也可以用python语句来描述上述过程:
>>> lst = []
>>> for i in [1, 2, 3, 4]:
... if i % 2 == 0:
... lst = lst + [i**2]
...
>>> lst
[4, 16]
注意:列表推导式中的if
语句是可选的(也可以丢弃)
>>> [i**2 for i in [1, 2, 3, 4]]
[1, 4, 9, 16]
Tree 树
树
是表示信息层次结构的数据结构
文件系统是一个树结构的样例
如,在cs61a
文件夹中,一些文件夹将学生的projects
、lab
和homework
区分开
下一层文件夹则区分不同的作业的文件夹,如hw01
, lab01
, hog
等等
在那些文件夹中,又包含了starter files
和ok
如下提供了一个cs61a
的不完整目录
正如图中所示,与自然界中的树不同,树
的抽象数据类型时根在顶部,叶在底部
注:本节中的树采用列表之列表
(List of lists
)的形式表示
一些树的术语:
- 根(root):树顶部的节点
- 标签(label):节点的值,可以又
label
函数(选择器)得到 - 分支(branches):树的根节点下一层的树,可以又
branches
函数(选择器)得到 - 叶子(leaf):没有分支的树
- 节点(node):树中的任何位置(如根节点、叶节点等)
树
的抽象数据类型由根节点和一系列的分支组成
创建一颗树,获取根节点值,获取分支,需要使用系列构造器和选择器:
- 构造器(Constructor)
tree(label, branches=[])
:使用参数label
和列表branches
创建tree
对象- 注意:该构造器的第二个参数
branches
是可选的,若要创建一颗没有任何分支的树,将该参数留空即可
- 选择器(Selector)
label(tree)
:返回一颗树根节点的值branches(tree)
:返回给定的树的所有分支(子树列表)
- Convenience function
is_leaf(tree)
:若给定的树的分支为空则返回True
,否则返回False
例如,树可以由以下代码生成:
number_tree = tree(1,
[tree(2),
tree(3,
[tree(4),
tree(5)]),
tree(6,
[tree(7)])])
这颗树的图形如下:
1
/ | \
2 3 6
/ \ \
4 5 7
为从该树种获取数字3
(该树第二个分支的根节点的值),可以使用如下方法:
label(branches(number_tree)[1])
函数print_tree
以可以由人类阅读的方式打印一颗树,具体形式为:
- 根节点不缩进
- 该节点的每个分支均比根节点缩进一级
def print_tree(t, indent=0):
"""Print a representation of this tree in which each node is
indented by two spaces times its depth from the root.
>>> print_tree(tree(1))
1
>>> print_tree(tree(1, [tree(2)]))
1
2
>>> numbers = tree(1, [tree(2), tree(3, [tree(4), tree(5)]), tree(6, [tree(7)])])
>>> print_tree(numbers)
1
2
3
4
5
6
7
"""
print(' ' * indent + str(label(t)))
for b in branches(t):
print_tree(b, indent + 1)
Required Question 必答问题
Lists 列表
Q1: Coordinate 坐标
实现一个名为coords
的函数,它接受一个函数fn
、一个序列seq
和函数fn
输出值的下限lower
和上限upper
作为参数
返回一个坐标对(该坐标对为一个二元列表)的列表,使得:
- 每个(x, y)对被表示为[x, fn(x)]
- x坐标是序列中的元素
- 结果仅包含y坐标在下限和上限之间(含)的坐标对
doctest
给出的示例如下
"""
>>> seq = [-4, -2, 0, 1, 3]
>>> fn = lambda x: x**2
>>> coords(fn, seq, 1, 9)
[[-2, 4], [1, 1], [3, 9]]
"""
该问题限制只能一行完成,函数可以用列表推导式实现
def coords(fn, seq, lower, upper):
"*** YOUR CODE HERE ***"
return [[x, fn(x)] for x in seq if lower <= fn(x) <= upper]
以上实现可以通过脚本测试
$ python3 ok -q coords --local
=====================================================================
Assignment: Lab 5
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Q2: Riffle Shuffle 洗牌
假设有一副牌(或一串东西),欲要通过洗牌,得到一种新的排列方式:
- 顶部的牌接着是中间的牌,然后是第二张牌,再是中间之后的牌,以此类推
- 假设牌(或序列)中有偶数张牌,请编写一个列表推导式,用于生成洗牌后的序列
doctest
如下:
"""
>>> riffle([3, 4, 5, 6])
[3, 5, 4, 6]
>>> riffle(range(20))
[0, 10, 1, 11, 2, 12, 3, 13, 4, 14, 5, 15, 6, 16, 7, 17, 8, 18, 9, 19]
"""
对样例的分析如下
0 | 1 | 2 | 3 | |
---|---|---|---|---|
原列表 | 3 | 4 | 5 | 6 |
洗牌后列表 | 3 | 5 | 4 | 6 |
新列表中各数字在原列表中的索引 | 0 | 2 | 1 | 3 |
新列表中各数字在新列表中的索引 | 0 | 1 | 2 | 3 |
两索引之间的关系 | 0 | 0 + 2 | 1 | 1 + 2 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
原列表 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
洗牌后列表 | 0 | 10 | 1 | 11 | 2 | 12 | 3 | 13 | 4 | 14 | 5 | 15 | 6 | 16 | 7 | 17 | 8 | 18 | 9 | 19 |
新列表中各数字在原列表中的索引 | 0 | 10 | 1 | 11 | 2 | 12 | 3 | 13 | 4 | 14 | 5 | 15 | 6 | 16 | 7 | 17 | 8 | 18 | 9 | 19 |
新列表中各数字在新列表中的索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
两索引之间的关系 | 0 | 0 + 10 | 1 | 1 + 10 | 2 | 2 + 10 | 3 | 3 + 10 | 4 | 4 + 10 | 5 | 5 + 10 | 6 | 6 + 10 | 7 | 7 + 10 | 8 | 8 + 10 | 9 | 9 + 10 |
假设一个数字,在新列表中的索引为i
,那么由上述样例可知,该数字在原列表中的索引应为:
- 先将洗牌后的列表分组,每相邻两个分为一组,该数字的其组号的关系为,即
i // 2
- 在每一组中
- 第一个数字在原列表中的索引即为其组号
i // 2
- 第二个数字在原列表中的索引为:
组号 + 列表长度的一般
,即i // 2 + length //2
- 第一个数字在原列表中的索引即为其组号
- 由于每一组数字中第一个数字为偶数(
i % 2 == 0
),第二个数字为奇数(i % 2 == 1
)- 则该数字在新列表中索引与在原列表中索引的关系可以表示为:
i // 2 + i % 2 * (length //2)
- 则该数字在新列表中索引与在原列表中索引的关系可以表示为:
函数的实现如下:
def riffle(deck):
"""Produces a single, perfect riffle shuffle of DECK, consisting of
DECK[0], DECK[M], DECK[1], DECK[M+1], ... where M is position of the
second half of the deck. Assume that len(DECK) is even.
"""
"*** YOUR CODE HERE ***"
return [deck[i//2 + i % 2 * (len(deck)//2)] for i in range(len(deck))]
可以通过脚本测试
$ python3 ok -q riffle --local
=====================================================================
Assignment: Lab 5
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Trees
Q3: Finding Berries
定义一个名为函数berry_finder
,该函数接受一棵树作为输入
若树中包含一个值为’berry’的节点则返回True,否则返回False。
提示:使用for
循环遍历每一个分支
doctest
如下:
"""
>>> scrat = tree('berry')
>>> berry_finder(scrat)
True
>>> sproul = tree('roots', [tree('branch1', [tree('leaf'), tree('berry')]), tree('branch2')])
>>> berry_finder(sproul)
True
>>> numbers = tree(1, [tree(2), tree(3, [tree(4), tree(5)]), tree(6, [tree(7)])])
>>> berry_finder(numbers)
False
>>> t = tree(1, [tree('berry',[tree('not berry')])])
>>> berry_finder(t)
True
"""
本题思路为:
- 检查根节点值是否为
'berry'
,是则返回True
- 若不是,则分别在各个分支(子树)中查找
'berry'
- 若找到,则返回
True
- 若找到,则返回
- 当所有分支均为找到,则返回
False
函数实现如下:
def berry_finder(t):
"""Returns True if t contains a node with the value 'berry' and
False otherwise.
"""
"*** YOUR CODE HERE ***"
if label(t) == 'berry':
return True
for branch in branches(t):
if berry_finder(branch):
return True
return False
以上函数实现可以通过脚本测评
$ python3 ok -q berry_finder --local
=====================================================================
Assignment: Lab 5
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Q4: Sprout leaves
要求实现一个函数sprout_tree
该函数接受一棵树t
和一个列表leaves
为参数
返回一棵与t
相同的新树,但其中每个旧的叶子节点都有一系列新的分支,每个分支对应于leaves中的一个元素
如,首先生成一颗树t = tree(1, [tree(2), tree(3, [tree(4)])])
::
1
/ \
2 3
|
4
若调用sprout_leaves(t, [5, 6])
,生成的结果如下(*
之间的为新添加的叶子):
1
/ \
2 3
/ \ |
*5* *6* 4
/ \
*5* *6*
doctest
如下:
"""
>>> t1 = tree(1, [tree(2), tree(3)])
>>> print_tree(t1)
1
2
3
>>> new1 = sprout_leaves(t1, [4, 5])
>>> print_tree(new1)
1
2
4
5
3
4
5
>>> t2 = tree(1, [tree(2, [tree(3)])])
>>> print_tree(t2)
1
2
3
>>> new2 = sprout_leaves(t2, [6, 1, 2])
>>> print_tree(new2)
1
2
3
6
1
2
"""
要求不能直接对原始数据类型(即列表)进行操作,限制只能使用给定的构造器和选择器
需要明确以下几点:
- 假设一棵树的根节点不是叶子节点,那么该树的叶子节点,也是该树的所有子树的所有叶子节点,即若在一棵树的叶子节点上添加分支,则相当于在该树的所有子树上添加分支
- 函数
sprout_tree
的功能为返回一颗添加给定叶子节点后的树 - 树的构造器
tree(label, branches=[])
中,第一个参数为根节点的值,第二个参数为由其所有子树组成的列表,即branches
中的每一个元素都是一颗树
由上可知,我们有如下思路实现函数sprout_tree
:
- 若参数
t
没有分支,即给定的树只有一个根节点(同时也是叶子节点),则:- 以列表
leaves
中的每个元素为节点值生成一颗树,每颗树共同组成一个列表branches
- 创建一颗树,以原
t
的值label
为根节点的值,以上一步中得到的列表branches
为分支创建一颗新的树new_tree
- 返回
new_tree
- 以列表
- 若参数
t
有分支,则:- 对参数
t
分支中的每一个子树调用sprout_tree
(添加leaves
),其返回的树共同构成子树``branches` - 以原
t
的值label
为根节点的值,以上一步中得到的列表branches
为分支创建一颗新的树new_tree
- 返回
new_tree
- 对参数
函数的实现如下:
def sprout_leaves(t, leaves):
"""Sprout new leaves containing the data in leaves at each leaf in
the original tree t and return the resulting tree.
"""
"*** YOUR CODE HERE ***"
if is_leaf(t):
return tree(label(t), [tree(leaf) for leaf in leaves])
return tree(label(t), [sprout_leaves(branch, leaves) for branch in branches(t)])
以上实现满足评分脚本要求
python3 ok -q sprout_leaves --local
=====================================================================
Assignment: Lab 5
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Q5: Don’t violate the abstraction barrier
这一部分没有要求写任何代码
而是要求在编写使用ADT的函数时,尽可能使用构造器和选择器,而不是对底层数据进行操作
关于抽象屏障,已在lab 04 Q6中进行过相关说明
本节要求使用脚本检测实现是否违反抽象障碍,抽象屏障的本质保证,只要正确使用构造函数和选择函数,更改ADT的实现不应影响使用该ADT的任何程序的功能。
$ python3 ok -q check_abstraction --local
=====================================================================
Assignment: Lab 5
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Optional Questions 可选问题
Q6: Add trees
定义函数 add_trees
这个函数的输入为两个树t1
和t2
返回值为一个新树:
- 这个树中某个节点的值等于
t1
和t2
对应节点的值求和 - 若
t1
和t2
中某一个树中的某个特定位置上存在节点,但另一个树中不存在该节点,则新树中该节点的值等于存在该节点的树中该节点的值
样例在doctest
给出:
"""
>>> numbers = tree(1,
... [tree(2,
... [tree(3),
... tree(4)]),
... tree(5,
... [tree(6,
... [tree(7)]),
... tree(8)])])
>>> print_tree(add_trees(numbers, numbers))
2
4
6
8
10
12
14
16
>>> print_tree(add_trees(tree(2), tree(3, [tree(4), tree(5)])))
5
4
5
>>> print_tree(add_trees(tree(2, [tree(3)]), tree(2, [tree(3), tree(4)])))
4
6
4
>>> print_tree(add_trees(tree(2, [tree(3, [tree(4), tree(5)])]), \
tree(2, [tree(3, [tree(4)]), tree(5)])))
4
6
8
5
5
"""
分析以下样例:
# case 1
1 1 1+1
/ \ / \ / \
2 5 2 5 2+2 5+5
/ \ / \ + / \ / \ = / \ / \
3 4 6 8 3 4 6 8 3+3 4+4 6+6 8+8
/ / /
7 7 7+7
# case 2
2 3 2+3
+ / \ = / \
4 5 4 5
# case 3
2 2 2+2
/ + / \ = / \
3 3 4 3+3 4
# case 4
2 2 2+2
/ + / \ / \
3 3 5 = 3+3 5
/ \ / / \
4 5 4 4+4 5
课程组给出提示:可能需要使用python内置的zip
函数来同时迭代多个序列
思路如下:
- 接收两棵树
t1
和t2
作为参数 - 对于根节点,将
t1
和t2
根节点的值相加,作为新树的根节点值 - 对于
t1
和t2
的各个子树:- 使用
zip
函数打包两个序列,分别对相应的子树(如(t1_branch1, t2_branch1)
)调用add_tree
(即回到第一步)得到一个子树序列 - 若
t1
和t2
的子树个数不等,取子树较多的树的分支的剩余子树 - 将上两步得到的两个序列合并
- 使用
- 已
2.
得到的值为根节点值,以3.
得到的序列为子树序列构造一颗新的树,该树即所求的树
函数如下:
def add_trees(t1, t2):
"*** YOUR CODE HERE ***"
def get_branches(t1, t2):
branch_t1 = branches(t1)
branch_t2 = branches(t2)
new_branches = [add_trees(t1_branch, t2_branch) for t1_branch, t2_branch in zip(branch_t1, branches(t2))]
if len(branch_t1) < len(branch_t2):
start = len(branch_t1)
new_branches += branch_t2[start:]
elif len(branch_t1) > len(branch_t2):
start = len(branch_t2)
new_branches += branch_t1[start:]
return new_branches
return tree(label(t1)+label(t2), get_branches(t1, t2))
以上实现可以通过脚本测评
$ python3 ok -q add_trees --local
=====================================================================
Assignment: Lab 5
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Shakespeare and Dictionaries 莎士比亚与字典
本节将使用python内置的数据结构字典
来近似莎士比亚的作品
将使用一个二元语言模型,思路如下:
- 从某个单词开始,如单词
the
- 遍历莎士比亚所有的文本,对于每个
the
的实例,记录其之后的单词,并将其添加到一个列表中,称该列表为the
的后继,假设已对所有单词完成该操作 - 随机选择该列表中的一个单词,如
cat
,查找cat
的后继,随机选择一个单词,重复进行该过程,直到遇到一个.
或!
或?
结束,生成一个莎士比亚句子
在上述过程中,将查找的对象称为“后继表”,它是一个python字典,在该字典中,键是单词,而值是那些单词的后继列表
Q7: Successor Tables
本节要求实现build_successors_table
函数
输入是单词列表(对应于莎士比亚文本)
输出是后继表(successors table)
注意:默认每一句的第一个单词为.
的后继
示例如下:
"""
>>> text = ['We', 'came', 'to', 'investigate', ',', 'catch', 'bad', 'guys', 'and', 'to', 'eat', 'pie', '.']
>>> table = build_successors_table(text)
>>> sorted(table)
[',', '.', 'We', 'and', 'bad', 'came', 'catch', 'eat', 'guys', 'investigate', 'pie', 'to']
>>> table['to']
['investigate', 'eat']
>>> table['pie']
['.']
>>> table['.']
['We']
"""
注:共需要完成两个代码段,即两个"***YOUR CODE HERE ***"
。
def build_successors_table(tokens):
"""Return a dictionary: keys are words; values are lists of successors.
"""
table = {}
prev = '.'
for word in tokens:
if prev not in table:
"*** YOUR CODE HERE ***"
table[prev] = []
"*** YOUR CODE HERE ***"
table[prev] += [word]
prev = word
return table
函数实现满足测评脚本要求
$ python3 ok -q build_successors_table --local
=====================================================================
Assignment: Lab 5
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Q8: Construct the Sentence
本节实现一个函数用于生成一些句子
- 假设有一个起始单词
word
- 在我们的表
table
中查找这个单词,以找到它的后继列表 - 随机从这个列表中选择一个单词作为句子中的下一个单词,返回第一步,直到遇到一个终止符
.
,?
,!
提示:要从列表中随机选择,使用import random
导入Pythonrandom
库,使用表达式random.choice(my_list)
doctest
如下:
"""
>>> table = {'Wow': ['!'], 'Sentences': ['are'], 'are': ['cool'], 'cool': ['.']}
>>> construct_sent('Wow', table)
'Wow!'
>>> construct_sent('Sentences', table)
'Sentences are cool.'
"""
函数实现如下:
def construct_sent(word, table):
"""Prints a random sentence starting with word, sampling from
table.
"""
import random
result = ''
while word not in ['.', '!', '?']:
"*** YOUR CODE HERE ***"
result += word + " "
word = random.choice(table[word])
return result.strip() + word
以上实现满足测评脚本要求
$ python3 ok -q construct_sent --local
=====================================================================
Assignment: Lab 5
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
最后,课程组提供了一些函数将Q7
和Q8
的实现结合了起来,使用一些现实数据来运行函数
以下代码段返回一个包含莎士比亚所有单词的列表
def shakespeare_tokens(path='shakespeare.txt', url='http://composingprograms.com/shakespeare.txt'):
"""Return the words of Shakespeare's plays as a list."""
import os
from urllib.request import urlopen
if os.path.exists(path):
return open('shakespeare.txt', encoding='ascii').read().split()
else:
shakespeare = urlopen(url)
return shakespeare.read().decode(encoding='ascii').split()
以下两行代码运行了上述函数并建立了一个后继列表
tokens = shakespeare_tokens()
table = build_successors_table(tokens)
课程组实现了一个利用后继列表生成句子的函数
>>> def sent():
... return construct_sent('The', table)
>>> sent()
" The plebeians have done us must be news-cramm'd."
>>> sent()
" The ravish'd thee , with the mercy of beauty!"
>>> sent()
" The bird of Tunis , or two white and plucker down with better ; that's God's sake."
上述函数生成的句子均以The
打头,对上述实现做出一定的修改,使得句子以一个随机的单词开头,函数如下:
def random_sent():
import random
return construct_sent(random.choice(table['.']), table)
上述函数可以随机生成一个莎士比亚句子
>>> random_sent()
' Long live by thy name , then , Dost thou more angel , good Master Deep-vow , And tak'st more ado but following her , my sight Of speaking false!'
>>> random_sent()
' Yes , why blame him , as is as I shall find a case , That plays at the public weal or the ghost.'