SICP 解题集 —— 第 1 章 构造过程抽象
1 练习 1.1
2 练习 1.2
3 练习 1.3
4 练习 1.4
5 练习 1.5
6 练习 1.6
7 练习 1.7
8 练习 1.8

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) ,它由三个子表达式组成:

其中第一个子表达式是要被调用的过程,而第二和第三个都是参数。

这确实与书中之前见过的所有代码都不同。之前的代码中,要被调用的过程,即第一个子表达式,都被表示成一个普通的名字,如 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-ifabc 这四个表达式全都求值,然后再去执行 new-if 的过程体。

if 作为特殊形式,是有着它独特的求值规则的:对 (if a b c) 求值时,解释器会先对 a 求值,然后根据结果,选择对 bc 中的其中一个求值。

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