SICP 解题集 —— 第 1 章 构造过程抽象
更新日期:2026-03-10
1 练习 1.1
只是按顺序执行一些 Scheme 代码。结果如下:
> 10 10
> (+ 5 3 4) 12
> (- 9 1) 8
> (/ 6 2) 3
> (+ (* 2 4) (- 4 6)) 6
> (define a 3) > (define b (+ a 1)) > (+ a b (* a b)) 19
> (= a b) #f
> (if (and (> b a) (< b (* a b))) b a) 4
> (cond ((= a 4) 6) ((= b 4) (+ 6 7 a)) (else 25)) 16
> (+ 2 (if (> b a) b a)) 6
> (* (cond ((> a b) a) ((< a b) b) (else -1)) (+ a 1)) 16
注意:正文中说过,特殊形式 define 返回的值是不确定的。解释器可以不打印任何值。
2 练习 1.2
如下:
> (/ (+ 5 4 (- 2 (- 3 (+ 6 (/ 4 5))))) (* 3 (- 6 2) (- 2 7))) -37/150
3 练习 1.3
如下:
在 Scheme 中,分号表示代码中的注释,从分号开始到该行行尾的内容都会被解释器忽略。
关于代码中的方括号,见主页面里的说明。
> (define (sum-of-bigger-two a b c) (cond [(and (< a b) (< a c)) (+ b c)] [(and (< b a) (< b c)) (+ a c)] [else (+ a b)])) ; 三个情况分别对应 a、b 和 c 是最小值时的情况 > (sum-of-bigger-two (- 5) (- 2) 4) 2
> (sum-of-bigger-two (- 5) 4 (- 2)) 2
> (sum-of-bigger-two (- 2) 4 (- 5)) 2
4 练习 1.4
根据正文里的表述,在求值一个组合式时,第一步是求值该组合式的各个子表达式。
我们考察过程体中这个表达式 ((if (> b 0) + -) a b) ,它由三个子表达式组成:
(if (> b 0) + -)
a
b
其中第一个子表达式是要被调用的过程,而第二和第三个都是参数。
这确实与书中之前见过的所有代码都不同。之前的代码中,要被调用的过程,即第一个子表达式,都被表示成一个普通的名字,如 square 或 + 。但在这里,要被调用的过程,不再是一个简单名字,而是一个组合式。“求值各个子表达式”,意味着这三个子表达式都要被求值,包括第一个子表达式——这个组合式。虽然在逻辑上,第一个和后面两个不同,但它们都将被一视同仁地求值。于是,第一个子表达式也被(递归地)求值。若 b 大于 0 ,则它的结果是 + ,这相当于对 (+ a b) 求值。否则,相当于对 (- a b) 求值。这样,便计算出了 b 的绝对值与 a 的和。
5 练习 1.5
解释器采用应用序求值时:
在执行 test 过程之前,解释器必须先去求值第二个参数 (p) ,为了得到它的结果。但不幸地, p ,这个没有参数的过程,它只会调用自己,形成无限递归。由于是尾递归,所以计算过程会被优化成常数空间的迭代,也就是通常所说的死循环(这部分内容正文后续部分即将会讲到)。因此,解释器陷入死循环,永远不能结束计算,除非强行停止。
解释器采用正则序求值时:
无需对第二个参数 (p) 求值,解释器就能直接开始执行 test 的过程体。于是对 (if (= 0 0) 0 (p)) 求值。注意题中明确指出 if 的求值规则仍然是原样,于是对谓词 <predicate> 先求值。在得到 #t ,即真值之后,根据求值规则,解释器只会对 <consequent> ,也就是 0 ,求值,而根本不会对 <alternative> ,也就是 (p) ,求值。因此,解释器不会陷入无限递归 / 循环,而能够正常求出并打印 0 。
6 练习 1.6
原书在 4.2.1 节再次提到了这道练习 1.6。那一节里做出了一个类似的操作,定义了 unless 过程,之后也进行了讲解。
重点: if 是特殊形式,而 new-if 只是一个普通的过程。
我们使用的 Scheme 解释器是采用应用序求值的,所以在对 (new-if a b c) 求值时,解释器会先对 new-if 、 a 、 b 和 c 这四个表达式全都求值,然后再去执行 new-if 的过程体。
而 if 作为特殊形式,是有着它独特的求值规则的:对 (if a b c) 求值时,解释器会先对 a 求值,然后根据结果,选择对 b 和 c 中的其中一个求值。
在 sqrt-iter 里,原本的 if 中, <alternative> 是一个(尾)递归调用。在谓词 <predicate> 为真时,根据上面的规则,这个递归调用便不会被求值,从而使程序能够停止。使用 new-if 替换 if 之后,这个递归调用无论如何都会被求值,从而造成无限递归。此外,由于 new-if 是一个普通的过程,所以这个递归调用不再是尾递归了,计算过程不能被强制优化成常数空间的迭代计算过程,因此解释器会消耗越来越多的内存空间,最终导致错误。
7 练习 1.7
为了获取两次猜测之间的“改变值”,对于一些过程我们要新增一个参数 previous-guess ,记录上一次的猜测值。至于初始时的“上次猜测值”,这里设置成 2.0 。设置成其他值也可以,只要和 1.0 相差足够大,从而能够进入递归(循环)。
在 ratio-sqrt-iter 中,进行(尾)递归调用时, guess 成为下一轮的 previous-guess ,而下一轮的 guess 则由 improve 过程计算得出。
下一节(1.1.8)指出,定义在全局的过程会占用名字,这个问题其实这里已经能感受到了:为了避免和正文里的过程重名,我们添加了一些“ratio-”前缀。
> (define (ratio-good-enough? previous-guess guess) (< (abs (/ (- guess previous-guess) guess)) 0.001))
> (define (ratio-sqrt-iter previous-guess guess x) (if (ratio-good-enough? previous-guess guess) guess (ratio-sqrt-iter guess (improve guess x) x)))
> (define (ratio-sqrt x) (ratio-sqrt-iter 2.0 1.0 x)) > (ratio-sqrt 9) 3.000000001396984
> (ratio-sqrt (+ 100 37)) 11.704699917758145
> (ratio-sqrt (+ (ratio-sqrt 2) (ratio-sqrt 3))) 1.7737712336472033
> (square (ratio-sqrt 1000)) 1000.000369924366
8 练习 1.8
和正文中几乎一样,只有公式不同。反映在代码中,就是 cbrt-improve 的过程体。
> (define (cube x) (* x x x))
> (define (cbrt x) (cbrt-iter 1.0 x))
> (define (cbrt-iter guess x) (if (cbrt-good-enough? guess x) guess (cbrt-iter (cbrt-improve guess x) x)))
> (define (cbrt-improve guess x) (/ (+ (/ x (square guess)) (* 2 guess)) 3))
> (define (cbrt-good-enough? guess x) (< (abs (- (cube guess) x)) 0.001)) > (cbrt 8) 2.000004911675504
> (cbrt 27) 3.0000005410641766
> (cbrt 2) 1.259933493449977
> (cube (cbrt 2)) 2.0000592593226547