SICP,等我(2011-08-04)

TMD谁把 list 翻译成“列表”的,明明是“序列”的意思嘛
——Pope, 2012


本科的时候啃过SICP,好像只读完了前两章。现在忙了,反而觉得应该好好看看,连以前不入眼的练习也决定一道道过。

解释器是用的DrRacket,前身是DrScheme。不知道为啥改了这么个怪名字-_-!

练习1.2

; Exercise 1.2  
(/ (+ 5  
      4  
      (- 2  
         (- 3  
            (+ 6  
               (/ 4 5)))))  
   (* 3  
      (- 6 2)  
      (- 2 7)))  

让我惊奇的是DrScheme居然返回-37/150!后来才知道Scheme里有种类型叫rational numbers。

练习1.3

; Exercise 1.3  
(define (single? lst)  
  (null? (cdr lst)))  

(define (minimum lst)  
  (if (single? lst)  
      (car lst)  
      (min (car lst)  
           (minimum (cdr lst)))))  

(define (sum-of-larger a b c)  
  (- (+ a b c)  
     (minimum (list a b c))))  
(sum-of-larger 2 4 5)  

Scheme用起来不爽的原因:

一是很多“脚手架”都得自己搭,比如,判断是否为单一元素(single?)、序列的尾(有“头(first)”,但是木有尾(tail));

二是提供的“脚手架”不好用,比如,求序列的最大/最小值,参数居然不能是序列,非得把要比较的元素一个个传进去,像这样(min 1 2 3)(当然,你也可以(apply min '(1 2 3)),但毕竟要转一道弯)。

看看人家Ruby, [1, 2, 3].first, [1, 2, 3].last, [1, 2, 3].min,多会来事

练习1.5

; Exercise 1.5  
(define (p) (p))  
(define (test x y)  
  (if (= x 0)  
      0  
      y))  

;(test 0 (p))  

练习1.5 很有意思(和后面的1.6一样)考察你对正则序(normal order)和应用序(applicative order)的理解。按照正则序求值是“完全展开而后规约”,而目前解释器实际采用的是“先对参数求值,而后将其代人到表达式中的所有同一参数”的应用序求值。应用序的好处就是可以避免重复对同一参数求值。

不管过程如何,先代到DrScheme里跑一遍。结果死循环,栈溢出了。稍作分析即可知道,DrScheme采用的是应用序,会先对参数求值。这样,0还是0,但对函数调用(p)求值却会进入死循环,直到栈溢出。如果解释器采用了正则序就会立即返回0,因为采用了正则序的解释器会先把(test 0 (p))展开为

(if (= 0 0)  
    0  
    (p))  

而if是预置函数,采用的是短路求值,即一旦判断为真就不会考虑else-子表达式。这样,采用了正则序的解释器安全返回0,结束求值过程。

但这并不是说正则序比应用序好,你总能针对某种策略做出假死的情况来。

; 1.1.7  
(define (sqrt-iter guess x)  
  (if (good-enough? guess x)  
      guess  
      (sqrt-iter (improve guess x)  
                 x)))  
(define (improve guess x)  
  (average guess (/ x guess)))  
(define (average x y)  
  (/ (+ x y) 2))  
(define (good-enough? guess x)  
  (< (abs (- (square guess) x)) 0.001))  
(define (square x) (* x x))  
(define (sqrt x)  
  (sqrt-iter 1.0 x))  

(sqrt 9)  

练习1.6

; Exercise 1.6  
(define (new-if predicate then-clause else-clause)  
  (cond (predicate then-clause)  
        (else else-clause)))  
(define (sqrt-iter2 guess x)  
  (new-if (good-enough? guess x)  
          guess  
          (sqrt-iter2 (improve guess x)  
                     x)))  
(define (sqrt2 x)  
  (sqrt-iter2 1.0 x))  
;(sqrt2 9)  

和练习1.5相似。不同的是,这里想强调一点,“管你是数据还是函数,众生平等,但有些函数比其他函数更平等”。这部分函数就是预置函数。虽然new-if功能上与if完全一样,但new-if毕竟是个普通函数,所有传入的参数都会先行求值(应用序嘛);if就不一样了,人家是大妈生的,只在判决出来了,才选择性地对子表达式求值,所以不会陷入无穷递归(在本题中)。

练习1.7

; Exercise 1.7  
(define (sqrt-iter3 guess old-guess x)  
  (if (good-enough2? guess old-guess x)  
      guess  
      (sqrt-iter3 (improve guess x)  
                  guess  
                  x)))  
(define (good-enough2? guess old-guess x)  
  (< (abs (- 1 (/ guess old-guess))) 0.01))  
(define (sqrt3 x)  
  (sqrt-iter3 1.0 x x))  

(sqrt3 2)  

练习1.8

; Exercise 1.8  
(define (cbrt-iter guess old-guess x)  
  (if (good-enough2? guess old-guess x)  
      guess  
      (cbrt-iter (cb-improve guess x)  
                 guess  
                 x)))  
(define (cb-improve guess x)  
  (/ (+ (/ x (square guess))  
        (* 2 guess))  
     3))  
(define (cbrt x)  
  (cbrt-iter 1.0 x x))  

(cbrt 8)  

练习1.9

; Exercise 1.9  
; (define (+ a b)  
;   (if (= a 0)  
;       b  
;       (inc (+ (dec a) b))))  
; (+ 4 5)  
; (inc (+ 3 5))  
; (inc (inc (+ 2 5)))  
; (inc (inc (inc (+ 1 5))))  
; (inc (inc (inc (inc (+ 0 5)))))  
; (inc (inc (inc (inc 5))))  
; (inc (inc (inc 6)))  
; (inc (inc 7))  
; (inc 8)  
; 9  

; (define (+ a b)  
;   (if (= a 0)  
;       b  
;       (+ (dec a) (inc b))))  
; (+ 4 5)  
; (+ 3 6)  
; (+ 2 7)  
; (+ 1 8)  
; (+ 0 9)  
; 9  

练习1.10

; Exercise 1.10  
(define (A x y)  
  (cond ((= y 0) 0)  
        ((= x 0) (* 2 y))  
        ((= y 1) 2)  
        (else (A (- x 1)  
                 (A x (- y 1))))))  
(A 1 10)  
(A 2 4)  
(A 3 3)  

练习1.11

; Exercise 1.11  
; (define (f n)  
;   (if (< n 3)  
;       n  
;       (+ (f (- n 1))  
;          (* 2 (f (- n 2)))  
;          (* 3 (f (- n 3))))))  

(define (f n)  
  (define (iter a b c n)  
    (if (= n 2)  
        a  
        (iter (+ a (* 2 b) (* 3 c)) a b (- n 1))))  
  (iter 2 1 0 n))  
(f 5)  

练习1.12

; Exercise 1.12  
(define (tail lst)  
  (cond ((null? lst) '())  
        ((single? lst) (car lst))  
        (else (tail (cdr lst)))))  

(define (Pascal-triangle n)  
  (define (gen lst)  
    (define (last-line lst)  
      (cond ((single? lst) '())  
            (else (cons (+ (first lst)  
                           (second lst))  
                        (last-line (cdr lst))))))  
    (append lst  
            (list (cons 1  
                        (append (last-line (tail lst))  
                                '(1))))))  
  (cond ((= n 1) '(1))  
        ((= n 2) '((1) (1 1)))  
        (else (gen (Pascal-triangle (- n 1))))))  
(Pascal-triangle 6)  

这题我自己都不知道是怎么过的,感觉Scheme的序列很诡异,序列(list)和有序对(pair)到底谁是妈啊?!R6RS(Scheme标准)里倒是给出了序列的递归定义:序列要么是个空序列;要么是个有序对,其后一部分须为序列(A list can be defined recursively as either the empty list or a pair whose cdr is a list.)。照这个定义,严格地说,任何序列的最后一个元素都必须是空序列(空序列不是有序对),因为结尾的必须是序列嘛,没说可以是元素哦。但事实上,也不会抠这死理,这种不以空序列结尾的有序对链叫做improper list(它不是list
)。注意,(a b c . d) 和(a . (b . (c . d)))是等价的。

只有弄清了序列和有序对的区别,你才能搞清楚为什么(cdr '(1 2)) 返回(2),而(cdr '(1 . 2))返回2,以及为什么(cons 8 9) 产生的是一个有序对(8 . 9),而(cons 8 '(9))生成的却是序列(8 9)。

练习1.13

要求证Fib(n)是最接近Φ^n/√5的整数,即 | Fib(n)-Φ^n/√5 | < 1。而Fib(n) = (Φ^n-γ^n) /√5, Φ = (1 + √5)/2, γ = (1 - √5)/2, 所以Φ^n/√5 - Fib(n) = γ^n/√5 = (-1)^n ((√5-1)/2)^n/√5。众所周知,(√5-1)/2是黄金分割点0.618,其除以 √5肯定比1小,所以Φ^n/√5 - Fib(n)的绝对值小于1,故得证。

现在的问题是,你怎么知道Fib(n) = (Φn-γn) /√5

这是一个来自中学奥赛的古老技巧,不知道它是怎么来的,但它很优美地就把问题解决了,就好像从没存在过这个问题一样。

f_n = Fib(n),设f_n - αf_(n-1) = β(f_(n-1) - αf_(n-2) ),则α + β = 1- αβ = 1,即α = (1 + √5)/2, β = (1 - √5)/2(或者对调)

所以f_n = α^(n-1) + α^(n-2)β + ... + αβ^(n-2) + β^(n-1)(此处省去若干字)。

然后呢,我想了很久,曾试图拆成两半,利用 αβ = -1的特性来化为等比数列⋯⋯最后放弃了,因为结果很复杂。它不应长这个样子。

然后,我又看了看提示,突然意识到,曾几何时有这么个恒等式:

α^n - β^n = (α - β)(α^(n-1) + α^(n-2)β + ... + αβ^(n-2) + β^(n-1))

-_-!!

练习1.14

所需空间为调用树的高度,即n / min{coins[k]}

SICP,等我(2011-08-04)_第1张图片

其所需空间的阶为Θ(n)。

设C(n, k)为以k种硬币兑换n元的方案数,coins为一数组,其元素的值为各种硬币面值,则

C(n, k) = C(n, k-1) + C(n-coins[k], k)

然后,⋯⋯,好吧,我承认,我不知道该怎么解,甚至连大致的阶也猜不出来-_-!!

练习1.15

; Exercise 1.15  
(define (cube x) (* x x x))  
(define (p x) (- (* 3 x) (* 4 (cube x))))  
(define (sine angle)  
   (if (not (> (abs angle) 0.1))  
       angle  
       (p (sine (/ angle 3.0)))))  
(sine 12.15)  
(p (sine 4.05))  
(p (p (sine 1.35)))  
(p (p (p (sine 0.45))))  
(p (p (p (p (sine 0.15)))))  
(p (p (p (p (p (sine 0.05))))))  
⋯⋯  

可见p将被调用5次。
在求(sine a)的值时,需要至少n层栈,n满足:

a / 3^n < 0.1

即,n > log_3 10a

故需要的空间的阶为Θ(log n)。

因其计算过程分为递归展开至终止情况(即弧度不大于0.1)和回退两部分,故步数为所需空间的常数倍,即Θ(log n)。

为了得到调用sine的实际步数,添加一个参数c(作为累计量),返回结果变为一个有序对(正弦值, 调用次数):

(define (inc x) (+ x 1))  
(define (cube x) (* x x x))  
(define (p x) (- (* 3 x) (* 4 (cube x))))  
(define (sine angle c)  
   (if (not (> (abs angle) 0.1))  
       (cons angle (inc c))  
       (let ((res (sine (/ angle 3.0) (inc c))))  
        (cons (p (car res)) (cdr res)))))  
(sine 12.15 0) ; => (-0.39980345741334 . 6)  

练习1.16

; Exercise 1.16  
(define (fast-expt b n)  
  (define (expt-iter b counter product)  
    (cond ((= counter 0) 1)  
          ((= counter 1) (* b product))  
          ((even? n) (expt-iter (square b) (/ counter 2) product))  
          (else (expt-iter b (- counter 1) (* b product)))))  
  (expt-iter b n 1))  

以b^7为例

b^7 = b^6 ·b
= (b2)3 ·b
= (b2)2 ·(b^2 · b)
= (b4)1 ·(b^2 · b)
= b4 ·(b^2 · b)

斜体部分就是product,用来存储幂为奇数时产生的尾数。

练习1.17

; Exercise 1.17

(define (double n)  
  (+ n n))  
(define (halve n)  
  (/ n 2))  
(define (fast-prod a b)  
  (cond ((= a 0) 0)  
        ((even? a) (double (fast-prod (halve a) b)))  
        (else (+ (fast-prod (- a 1) b) b))))  
(fast-prod 4 3)  

; Exercise 1.18  
(define (double n)  
  (+ n n))  
(define (halve n)  
  (/ n 2))  
(define (fast-prod a b sum)  
  (cond ((= a 0) sum)  
        ((even? a) (fast-prod (halve a) (double b) sum))  
        (else (fast-prod (- a 1) b (+ b sum)))))  
(define (prod a b)  
  (fast-prod a b 0))  

(prod 5 4)  

俄罗斯农民算法是这样进行的:第一乘数不断除2下取整直至为1,第二乘数不断乘2,如此罗列下来。然后把所有第一乘数为偶数的行划去,接着把第二乘数那一列加起来,即为乘积。

5 4
2 8
1 16


....20

换个写法:

5 × 4
= 4 × 4 + 4
= 2 × 8 + 4
= 1 × 16 + 4
= 0 × 16 + (16 + 4)

sum就是用来存储第一乘数为奇数时的累加值。

练习1.19

; Exercise 1.19  
(define (square n)  
  (* n n))  
(define (fib n)  
  (fib-iter 1 0 0 1 n))  
(define (fib-iter a b p q count)  
  (cond ((= count 0) b)  
        ((even? count)  
         (fib-iter a  
                   b  
                   (+ (square p) (square q))     ; compute p'  
                   (+ (square q) (* 2 p q))      ; compute q'  
                   (/ count 2)))  
        (else (fib-iter (+ (* b q) (* a q) (* a p))  
                        (+ (* b p) (* a q))  
                        p  
                        q  
                        (- count 1)))))  

(fib 7)  

变换规则

a’ ← b q + a q + a p
b’ ← b p + a q

应用两次后,得:

a’ ← b Q + a Q + a P
b’ ← b P + a Q

其中,P = p^2 + q^2,Q = q^2 + 2 p q

谁想出来的,太GG神奇了!

(待续……)

你可能感兴趣的:(SICP,等我(2011-08-04))