家庭作业 5:链表、效率与 Scheme
截止时间:7 月 29 日(星期三)晚上 11:59
操作指南
下载 hw05.zip。在压缩包中,您会找到一个名为
hw05.py的文件,以及 ok 自动评分器的副本。
提交方式: 完成后,请将作业提交到 Gradescope。截止时间前可以多次提交,只有最后一次提交会被评分。请确认代码已成功提交;具体步骤参见 Lab 0 ,其中有更详细的作业提交说明。
使用 Ok: 如果对 Ok 的使用有任何疑问,请参阅 这份指南。
阅读材料: 您可能会发现以下参考资料很有用:
评分: 作业根据正确率评分。每个不正确的问题将使总分减少一分。 此家庭作业满分为 2 分。
每份 Scheme 作业都附带 61A Scheme 解释器。在终端输入下面的命令启动它: python3 scheme 。要载入一个 Scheme 文件,例如 f.scm, type python3 scheme -i f.scm;要退出 Scheme 解释器,输入
(exit).
推荐的 VS Code 扩展
建议安装 vscode-scheme 扩展,以便高亮显示括号。
安装前:
安装后:
此外,还可以安装 61a-bot VS Code 扩展(安装说明),它可用于 Scheme 作业;该机器人也集成在 ok.
必答题
链表
下面是该类的实现: Link class:
class Link:
"""A linked list is either a Link object or Link.empty
>>> s = Link(3, Link(4, Link(5)))
>>> s.rest
Link(4, Link(5))
>>> s.rest.rest.rest is Link.empty
True
>>> s.rest.first * 2
8
>>> print(s)
(3 4 5)
"""
empty = ()
def __init__(self, first, rest=empty):
assert rest is Link.empty or isinstance(rest, Link)
self.first = first
self.rest = rest
def __repr__(self):
if self.rest:
rest_repr = ', ' + repr(self.rest)
else:
rest_repr = ''
return 'Link(' + repr(self.first) + rest_repr + ')'
def __str__(self):
string = '('
while self.rest is not Link.empty:
string += str(self.first) + ' '
self = self.rest
return string + str(self.first) + ')'
| 可变链表 | 普通链表 | |
|---|---|---|
| Traits | - 用自定义对象表示: Link - 具有方法和属性 - 可变 |
- 用抽象数据类型表示 - 具有选择函数 - 不可变 |
| 示例 |
|
|
| 说明 |
Link 是可变的,因为我们创建了对象 Link ,其属性允许直接访问内部元素。因此可以修改元素,从而改变链表。 Link
|
link 是不可变的,因为选择函数只返回元素的值;我们无法修改这些值或给它们重新赋值。
|
问题 1:连接链表
编写函数 add_link ,它接收两个链表 link1 和 link2,并返回一个 new 链表,把第二个输入连接到第一个输入的末尾 link2 。 link1.
可以假设输入列表是浅层的,其中的元素都不是链表。
注意: You may not 假设两个输入列表长度相同。
挑战(选做):不要假设输入列表是浅层的,也就是说输入可以是嵌套链表。提示:使用内置函数
type。
def add_links(link1: Link, link2: Link) -> Link:
"""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(new)
(1 2 3 4 5)
>>> new2 = add_links(l2,l1)
>>> print(new2)
(3 4 5 1 2)
"""
"*** YOUR CODE HERE ***"
使用 Ok 测试你的代码:
python3 ok -q add_links
问题 2:可变映射
Implement deep_map_mut(func, s),它把函数 func 应用于链表中的每个元素 s。如果某元素本身也是链表,就递归地把函数 func 应用于其中的元素。
你的实现应当 mutate 原始链表。 不要创建任何新链表。 函数返回 None.
提示:可以使用内置函数
isinstance判断某个元素是否为链表。>>> s = Link(1, Link(2, Link(3, Link(4)))) >>> isinstance(s, Link) True >>> isinstance(s, int) False
构造检查:本题最后一个测试会检查函数没有创建新链表。如果无法通过该 doctest,请确认没有调用构造器来创建链表,例如:
s = Link(1)
def deep_map_mut(func, s: Link) -> None:
"""Mutates a deep link s by replacing each item found with the
result of calling func on the item. Does NOT create new Links (so
no use of Link's constructor).
Does not return the modified Link object.
>>> link1 = Link(3, Link(Link(4), Link(5, Link(6))))
>>> square = lambda x: x * x
>>> print(link1)
(3 (4) 5 6)
>>> link2 = Link(1, Link(Link(Link(2, Link(3))), Link(4)))
>>> double = lambda x: x * 2
>>> print(link2)
(1 ((2 3)) 4)
>>> # Disallow the use of making new Links before calling deep_map_mut
>>> Link.__init__, hold = lambda *args: print("Do not create any new Links."), Link.__init__
>>> try:
... deep_map_mut(square, link1)
... deep_map_mut(double, link2)
... finally:
... Link.__init__ = hold
>>> print(link1)
(9 (16) 25 36)
>>> print(link2)
(2 ((4 6)) 8)
"""
"*** YOUR CODE HERE ***"
使用 Ok 测试你的代码:
python3 ok -q deep_map_mut
效率
除了把算法分为指数、二次、线性、对数和常数级,我们还会介绍大 Theta 记号,但不深入讨论运行时间分析的细节。
这里有一点细微区别:CS 61A 按输入本身分析运行时间,而后续课程(如 61B、CS 170)会按输入的 size (即位数)分析。如果暂时觉得困惑,不必太担心;只需保持口径一致,考虑改变输入时函数运行时间如何变化。
本课程此前主要关注程序的 正确性 ——也就是程序能否产生正确输出。不过,计算机科学家也关心如何给问题设计 高效的 解法。量化效率的一种方法,是研究函数的 runtime 如何随输入变化。本课程以函数执行的操作次数衡量运行时间。
A function f(n) has...
- 具有常数运行时间,如果它的运行时间
f不依赖于n。其运行时间为 Θ(1). - 具有对数运行时间,如果它的运行时间正比于
flog(n)。其运行时间为 Θ(log(n)). - 具有线性运行时间,如果它的运行时间正比于
fn。其运行时间为 Θ(n). - 具有二次运行时间,如果它的运行时间正比于
fn^2。其运行时间为 Θ(n^2). - 具有指数运行时间,如果它的运行时间正比于
fb^n,其中某个量为常数b。其运行时间为 Θ(b^n).
示例 1: 计算下面的表达式只需一次乘法: square(1);计算另一个表达式也只需一次乘法: square(100)。一般来说,调用 square(n) 所需的操作次数是 常数 ,不会随输入变化 n。因此称 square 的运行时间复杂度为 Θ(1)。
| input | 函数调用 | 返回值 | 操作次数 |
|---|---|---|---|
| 1 | square(1) |
1*1 | 1 |
| 2 | square(2) |
2*2 | 1 |
| ... | ... | ... | ... |
| 100 | square(100) |
100*100 | 1 |
| ... | ... | ... | ... |
| n | square(n) |
n*n | 1 |
示例 2: 计算下面的表达式只需一次乘法: factorial(1),而另一个示例需要 100 次乘法。 factorial(100). As n 增大时,函数的运行时间也 factorial 增长 呈线性。因此称 factorial 的运行时间复杂度为 Θ(n).
| input | 函数调用 | 返回值 | 操作次数 |
|---|---|---|---|
| 1 | factorial(1) |
1*1 | 1 |
| 2 | factorial(2) |
2*1*1 | 2 |
| ... | ... | ... | ... |
| 100 | factorial(100) |
100*99*...*1*1 | 100 |
| ... | ... | ... | ... |
| n | factorial(n) |
n*(n-1)*...*1*1 | n |
示例 3: 考虑下面的函数:
def bar(n):
for a in range(n):
for b in range(n):
print(a,b)
对 bar(1) 求值只会产生一次 print 调用,而对 bar(100) 求值会产生 10,000 次 print 调用。随着输入 n 增大时,函数的运行时间也 bar 增长 呈二次增长。因此称 bar 的运行时间复杂度为 Θ(n^2).
| input | 函数调用 | 操作(打印)次数 |
|---|---|---|
| 1 | bar(1) |
1 |
| 2 | bar(2) |
4 |
| ... | ... | ... |
| 100 | bar(100) |
10000 |
| ... | ... | ... |
| n | bar(n) |
n^2 |
示例 4: 考虑下面的函数:
def rec(n):
if n == 0:
return 1
else:
return rec(n - 1) + rec(n - 1)
对 rec(1) 求值只需一次加法;对 rec(4) 求值会产生 2^4 - 1 = 15 次加法,如下图所示。
在求值过程中, rec(4)会有两次调用 rec(3)、四次调用 rec(2)、八次调用 rec(1)以及十六次调用 rec(0).
因此共有八个 rec(0) + rec(0)、四个 rec(1) + rec(1)、两个 rec(2) + rec(2)和一个 rec(3) + rec(3),总计 1 + 2 + 4 + 8 = 15 次加法。

As n 增大时,函数的运行时间也 rec 增长 呈指数增长。具体来说,函数的运行时间在输入 rec 增加 1 时大约翻倍。 n 因此称其为指数复杂度。 rec 的运行时间复杂度为 Θ(2^n).
| input | 函数调用 | 返回值 | 操作次数 |
|---|---|---|---|
| 1 | rec(1) |
2 | 1 |
| 2 | rec(2) |
4 | 3 |
| ... | ... | ... | ... |
| 10 | rec(10) |
1024 | 1023 |
| ... | ... | ... | ... |
| n | rec(n) |
2^n | 2^n - 1 |
判断函数运行时间增长阶的提示:
- 如果函数是递归的,确定递归调用的次数以及每次递归调用的运行时间。
- 如果函数是迭代的,确定内层循环数量以及每个循环的运行时间。
- 忽略系数。执行某个线性次数操作的函数与执行另一个常数倍线性次数操作的函数都属于线性。
n100 * n - 选择最大的增长阶。如果函数第一部分为线性,第二部分为二次,那么整个函数的运行时间为二次。
本课程只考虑常数、对数、线性、二次和指数运行时间。
问题 3:素数
编写函数,在 O(sqrt(n)) 时间内判断一个数是否为素数,其中 sqrt 表示平方根。可以假设 n >= 2。
提示: 不需要检查每个小于 n 的数能否整除 n。
from math import sqrt
def is_prime_sqrt(n: int) -> bool:
"""Tests whether a number N is prime or not. Implement this function
in O(sqrt(n)) time. You can assume n >= 2
>>> is_prime_sqrt(2)
True
>>> is_prime_sqrt(67092481)
False
>>> is_prime_sqrt(524287)
True
>>> is_prime_sqrt(2251748274470911)
False
>>> is_prime_sqrt(6700417)
True
>>> is_prime_sqrt(44895587973889)
False
>>> is_prime_sqrt(2147483647)
True
>>> is_prime_sqrt(67280421310721)
True
"""
# sqrt(k) will give the square root of k as a floating point (decimal)
"*** YOUR CODE HERE ***"
使用 Ok 测试你的代码:
python3 ok -q is_prime_sqrt
Scheme
3 * (4 + 2),写作:
scm> (* 3 (+ 4 2))
18
与 Python 一样,调用表达式按以下步骤求值:
- 对运算符求值;结果应当是一个过程。
- 从左到右对各个操作数求值。
- 把该过程应用于已经求值的操作数。
下面是几个使用内置过程的例子:
scm> (+ 1 2)
3
scm> (- 10 (/ 6 2))
7
scm> (modulo 35 4)
3
scm> (even? (quotient 45 2))
#t
define 形式用于给符号绑定值,语法如下:
(define <symbol> <expression>)
scm> (define pi (+ 3 0.14))
pi
scm> pi
3.14
对下面的表达式求值: define 表达式时:
- 对最后一个子表达式(
<expression>)求值;在本例中结果为3.14. - 把该值绑定到符号(
symbol);在本例中符号为pi. - 返回这个符号。
The define 形式还可以定义新过程,详见“定义函数”一节。
cond 特殊形式可以包含多个谓词(类似 Python 的 if/elif):
(cond
(<p1> <e1>)
(<p2> <e2>)
...
(<pn> <en>)
(else <else-expression>))
每个子句的第一个表达式是谓词,第二个表达式是与该谓词对应的返回表达式。 else
子句可以省略;其中的 <else-expression> 是在所有谓词都不为真时使用的返回表达式。
求值规则如下:
- 按顺序对谓词
<p1>,<p2>, ...,<pn>求值,直到某个谓词得到真值(除#f). - 对第一个结果为真的谓词所对应的返回表达式求值,并返回其值。
- 如果所有谓词都不为真且存在
else子句,则对其返回表达式求值并返回。<else-expression>.
例如,下面这个 cond 表达式会返回离给定数最近的 3 的倍数: x:
scm> (define x 5)
x
scm> (cond ((= (modulo x 3) 0) x)
((= (modulo x 3) 1) (- x 1))
((= (modulo x 3) 2) (+ x 1)))
6
Q4: Pow
实现过程 pow ,计算数字 base 的非负整数次幂 exp。递归调用次数应随指数 pow 呈对数增长,而不是线性增长。 exp例如, (pow 2 32) 应只产生 5 次递归 pow 调用,而不是 32 次。 pow calls.
提示:
- x2y = (xy)2
- x2y+1 = x(xy)2
例如,216 = (28)2 and 217 = 2 * (28)2.
可以使用内置谓词
even?和odd?。此外,已经提供square过程。Scheme 没有
whileorfor语句,因此请使用递归解决本题。
(define (square n) (* n n))
(define (pow base exp)
'YOUR-CODE-HERE
)
使用 Ok 测试你的代码:
python3 ok -q pow
问题 5:反复立方
Implement repeatedly-cube,它接收数字 x 并把它反复立方指定次数。 n times.
下面是该过程应有行为的示例: repeatedly-cube
scm> (repeatedly-cube 100 1) ; 1 cubed 100 times is still 1
1
scm> (repeatedly-cube 2 2) ; (2^3)^3
512
scm> (repeatedly-cube 3 2) ; ((2^3)^3)^3
134217728
(define (repeatedly-cube n x)
(if (zero? n)
x
(begin
(define y ___)
___)))
使用 Ok 测试你的代码:
python3 ok -q repeatedly-cube
问题 6:Cadr 与 Caddr
定义过程 cadr,返回列表的第二个元素;再定义 caddr,返回列表的第三个元素。尝试使用 cadr 和 caddr 来定义前者。 car 和 cdr.
(define (cddr s)
(cdr (cdr s)))
(define (cadr s)
'YOUR-CODE-HERE
)
(define (caddr s)
'YOUR-CODE-HERE
)
使用 Ok 测试你的代码:
python3 ok -q cadr-caddr
在本地检查你的分数
你可以通过运行以下命令在本地检查此作业中每个问题的分数
python3 ok --score
这不会提交作业! 确认得分符合预期后,请将作业提交到 Gradescope,以获得作业学分。
提交作业
请上传所有你修改过的文件,将本次作业提交到 对应的 Gradescope 作业入口。 实验 00 中提供了详细说明。
考试练习
家庭作业还会附上往年考试题供你练习。这些题目无需提交;如果想多练习,可以自由尝试!