SICP Structure And Interpretation Of Computer Programs 中文第2版
分两部分 S 和 I
第一章 构造过程抽象
1,程序设计的基本元素
2,过程与他们所产生的计算
3, 用高阶函数做抽象
第二章 构造数据抽象
第三章 模块化、对象和状态
第四章 元语言抽象
第五章 寄存器机器里的计算
(心智的活动,学习。1,组合 简单认识组 为一个符合认识由此产生复杂认识。2,对比 两个认识放在一起对比,得到有关于相互关系的认识。3,将之隔离与其他认识,即抽象)
1,程序设计的基本元素:
1,表达式
2,命名和环境
3,组合式的求值
4,复合过程
5,过程应用的代换模型
(define (square x) (* x x)
应用序:先求值参数而后应用的方式,(自上而下)
正则序:完全展开而后归约
练习1.3 请定义一个过程,它以三个数为参数,返回其中较大的两个数之和
平方和
计算平方和的函数可以在书本第 8 页找到:
;;; p8-sum-of-squares.scm (define (sum-of-squares x y) (+ (square x) (square y)))
判断大数
『判断三个数中较大的两个』这个任务实际上就是执行以下决策树(假设三个数分别为 x
、 y
和 z
):
x < y / \ / \ / \ x < z y < z / \ / \ / \ / \ x < y x < y y < x y < x x < z z < x y < z z < y
这个决策树可以用 if
来表示:
(if (> x y) ; x 较大 (if (> y z) ; x 和 y 较大 ; x 和 z 较大) ; y 较大 (if (> x z) ; y 和 x 较大 ; y 和 z 较大))
也可以使用 cond
:
(cond ((and (> x y) (> y z)) ; x 和 y 较大) ((and (> x y) (> z y)) ; x 和 z 较大) ((and (> y x) (> x z)) ; y 和 x 较大) ((and (> y x) (> z x)) ; y 和 z 较大))
另一种办法是,写出 bigger
和 smaller
两个函数,它们分别接受两个数作为参数, bigger
返回两数的较大者,而 smaller
返回两数的较小者:
;;; 3-bigger-and-smaller.scm (define (bigger x y) (if (> x y) x y)) (define (smaller x y) (if (> x y) y x))
通过 bigger
和 smaller
,可以用一种更抽象的方式来选出三个数中较大的两个:
(define bigger (bigger x y)) (define another-bigger (bigger (smaller x y) z))
bigger-sum-of-squares
综合以上这些工作,就可以将整个函数写出来了:
;;; 3-bigger-sum-of-squares.scm (load "p8-sum-of-squares.scm") (load "3-bigger-and-smaller.scm") (define (bigger-sum-of-squares x y z) (sum-of-squares (bigger x y) ; bigger (bigger (smaller x y) z))) ; another bigger
bigger-sum-of-squares
接受三个数,并且计算较大两数的平方和。
1.7 实例:采用牛顿法求平方根
说明性描述what 与 行动性描述how
牛顿逐步逼近法
行动性描述 过程性语言
(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))
练习1.6
使用cond 将if定义为一个常规过程
(define (new-if predicate then-clause else-clause)
(cond (predicate then_clause)
(else else-clause))
试猜测用这个new-if重写求平方根的程序会发生什么?
(define (sqrt-iter guess x)
(new-if (good-enough? guess x)
guess
(sqrt-iter (improve guess x)
x)))
答案:返回: ;Aborting!: maximum recursion depth exceeded
超出了解释器最大递归深度
原因 使用new-if函数,无论predicate是真还是假,then-clasuse 和else-clause两个分支都会被求值,使用new-if重定义sqrt-iter,无论测试结果如何,sqrt-iter都会一直递归下去
练习1.7
使用good-enough?对于大数和小数会失败,解决策略是监测猜测值从上一次迭代到下一册迭代的变化情况,解释原因
答案:
可以发现,对于特别小的数,比如 0.00009
,书本给出的 sqrt
并不能计算出正确的答案; 而对于特别大的数,因为 mit-scheme 实现的小数精度不足以表示两个大数之间的差,所以 sqrt
会陷入死循环而无法得出正确结果。
要避免这一错误,我们按照练习所说,对 good-enough?
进行修改:不再检测猜测值 guess
的平方与 x
之间的差,而是检测新旧两次猜测值之间的比率,当比率变化非常小时,程序就停止 improve
。
新的 good-enough?
定义如下:
;;; 7-good-enough.scm (define (good-enough? old-guess new-guess) (> 0.01 (/ (abs (- new-guess old-guess)) old-guess)))
使用新的 good-enough?
重新定义 sqrt
函数 —— 大部分的代码和原来的一样,只是 sqrt-iter
和 good-enough?
两个函数更改了:
;;; 7-sqrt.scm (load "p16-sqrt.scm") ; 一定要先载入这些函数 (load "p15-improve.scm") ; 确保后面定义的重名函数可以覆盖它们 (load "p15-average.scm") (load "7-good-enough.scm") ; 载入新的 good-enough? (define (sqrt-iter guess x) (if (good-enough? guess (improve guess x)) ; 调用新的 good-enough? (improve guess x) (sqrt-iter (improve guess x) x)))
新的 sqrt
函数对于非常小和非常大的值都能给出正确答案:
1 ]=> (load "7-sqrt.scm") ;Loading "7-sqrt.scm"... ; Loading "p16-sqrt.scm"... ; Loading "p15-sqrt-iter.scm"... ; Loading "p15-good-enough.scm"... done ; Loading "p15-improve.scm"... ; Loading "p15-average.scm"... done ; ... done ; ... done ; ... done ; Loading "p15-improve.scm"... ; Loading "p15-average.scm"... done ; ... done ; Loading "p15-average.scm"... done ; Loading "7-good-enough.scm"... done ;... done ;Value: sqrt-iter 1 ]=> (sqrt 0.00009) ;Value: 9.486833049684392e-3 1 ]=> (sqrt 900000000000000000000000000000000000000000000000000000000000000000000000000000000000) ;Value: 9.486982144406554e41
Note
在新的 sqrt-iter
的定义中, (improve guess x)
被重复调用了多次,这是因为书本到这个练习为止还没有介绍 let
结构。
以下是一个使用 let
结构重写的、无重复调用的 sqrt-iter
:
(define (sqrt-iter old-guess x) (let ((new-guess (improve old-guess x))) (if (good-enough? old-guess new-guess) new-guess (sqrt-iter new-guess x)))) 练习1.8例题使用此公式:(x/(y^2) + 1) / 2 < 精确值若使用此公式求立方根,给出过程 原例题 (define (new-sqrt guess x) (if (good-enough? guess x) guess (sqrt-iter (improve guess x) x))) (define (improve guess x) (tri-average (* 2 guess) (/ x guess)))good-enough里改为cube即可 1.8 过程作为黑箱抽象 分解使每一个过程可以被用作定义其他过程的模块例如,基于square定义过程good-enough?,就是将square看做一个“黑箱”square纪委几个过程的抽象,即过程抽象过程用户不必去关心的细节:局部名: 形式参数的具体名字对过程体完全没有关系,称约束变量内部定义和块结构: 嵌套称为块结构 将x作为内部定义中的*变量,x有实际参数得到自己的值,称为词法作用域
2,过程与他们所产生的计算
过程所产生的计算过程的“形状” 及所消耗各种重要计算资源(时间空间)的速率
1,线性的递归和迭代阶乘 n!(define (factorial n) (if (= n 1) 1 (* n (factorial (- n 1)))))
递归:在展开阶段里, 这一计算过程构造起一个推迟进行的操作所形成的两条,收缩阶段表现为这些运算的实际执行这种类型的计算过程又一个推迟记性的运算链条刻画,称为一个递归计算过程推迟执行的乘法链条的长度(即位保存其轨迹需要保存的信息量)随n值而线性增长(正比于n) 过程是递归的,说明这个过程的定义中(直接或间接)引用了该过程本身
迭代: 可以用状态变量描述尾递归:在常量空间中执行含有迭代型计算过程的递归过程
练习 1.9inc:将参数增加1dec:将参数减少1(define (+ a b) (if (= a 0) b (inc (+ (dec a) b))))(define (+ a b) (if (= a 0) b (+ (dec a) (inc b))))请用代换模型展示这两个过程在求值(+ 4 5)时所产生的计算过程。区分是迭代还是递归》答案: 第一个调用本身了,是递归过程,计算过程是迭代 (plus 3 5)
进行求值,表达式的展开过程为:
(plus 3 5) (inc (plus 2 5)) (inc (inc (plus 1 5))) (inc (inc (inc (plus 0 5)))) (inc (inc (inc 5))) (inc (inc 6)) (inc 7) 8
从计算过程中可以很明显地看到伸展和收缩两个阶段,且伸展阶段所需的额外存储量和计算所需的步数都正比于参数 a
,说明这是一个线性递归计算过程。
另一个+
函数的定义如下(为了和内置的+
区分开来,我们将函数改名为plus
):
;;; 9-another-plus.scm
(load "9-inc.scm")
(load "9-dec.scm")
(define (plus a b)
(if (= a 0)
b
(plus (dec a) (inc b))))
对 (plus 3 5)
进行求值,表达式的展开过程为:
(plus 3 5) (plus 2 6) (plus 1 7) (plus 0 8) 8 从计算过程中可以看到,这个版本的plus
函数只使用常量存储大小,且计算所需的步骤正比于参数a
,说明这是一个线性迭代计算过程。
练习 1.10Ackermann函数(define (A x y) (cond ((= y 0) 0) ((= x 0) (* 2 y)) ((= y 1) 2) (else (A (- x 1) (A x (- y 1))))))
1 ]=> (load "10-ackermann.scm") ;Loading "10-ackermann.scm"... done ;Value: a 1 ]=> (A 1 10) ;Value: 1024 1 ]=> (A 2 4) ;Value: 65536 1 ]=> (A 3 3) ;Value: 65536
接着,我们通过给定一些连续的 n
,来观察题目给出的几个函数,从而确定它们的数学定义。
首先是 (define (f n) (A 0 n))
:
n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ... |
---|---|---|---|---|---|---|---|---|---|
值 | 0 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | ... |
可以看出,函数 f
计算的应该是 2* n
然后是 (define (g n) (A 1 n))
:
n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ... |
---|---|---|---|---|---|---|---|---|---|
值 | 0 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | ... |
可以看出,函数 g
计算的应该是2的n次方
最后是 (define (h n) (A 2 n))
:
n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ... |
---|---|---|---|---|---|---|---|---|---|
值 | 0 | 2 | 4 | 16 | 65536 | ... | ... | ... | ... |
超过4
之后,再调用函数h
就会出现超出解释器最大递归深度的现象 1.2.2树形递归 Fibonacci数序列 每个数都是前面两个数之和第一种方法,递归法,翻译定义 (define (fib n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (fib (- n 1)) (fib (- n 2))))))
此函数Fib(n)值的增长相对于n是指数的,Fib(n)就是最接近 (Φ^n) / √5 的整数
Φ = (1 + √5) / 2 =1.6180.... 即黄金分割的值
Φ^2 = Φ + 1
即该过程所用的计算步骤将随着输入增长而指数性的增长
空间需求是随着输入增长而线性增长
这样在计算中的每一点,只需保存书中在此之上的结点的轨迹
一般说,树形递归计算过程里所需的步骤数将正比与树中的结点数,其空间需求正比于数的最大深度。
第二种,迭代方法
int a b ,初始化为Fib(1) = 1 和 Fib(0) = 0
迭代规则:a = a + b
b = a
n次变后,a和b 分别等于Fib(n + 1) 和 Fib(n)
即:
(define (fib n)
(fib-iter 1 0 n))
(define (fib-iter a b count)
(if ( = count 0)
b
(fib-iter ( + a b) a (- count 1))))
第二种方法相对于n为线性的
实例:换零钱方式的统计
美元换为 半美元、四分之一美元、10美分、5美分 和 1美分
将总数为a的现金换成 n 种硬币的不同方式的数目等于
甲。将现金数 a 换成除第一种之外的所有其他硬币的不同方式数目,加上
乙。将现金 a-d 换成所有种类的硬币的不同方式数目,其中的 d 是第一种硬币的币值
a = 0 , 1种换零钱的方式
a < 0 , 0种
n = 0 , 0种
(define (count-change amount)
(cc amount 5))
(define (cc amount kinds-of-coins)
(cond ((= amount 0) 1)
((or (< amount 0) (= kinds-of-coins 0)) 0)
(else (+ (cc amount ; n次之后 甲数目
(- kinds-of-coins 1))
(cc (- amount ; n次之后 乙数目
(first-denomination kinds-of-coins))
kinds-of-coins)))))
(define (first-denomination kinds-of-coins)
(cond ((= kinds-of-coins 1) 1)
((= kinds-of-coins 2) 5)
((= kinds-of-coins 3) 10)
((= kinds-of-coins 4) 25)
((= kinds-of-coins 5) 50)))
练习1.11
函数 f 由如下的规则定义;如果 n < 3, 那么f (n) = n; 如果n>=3,那么f(n) =f(n-1)+2f(n-2) + 3f(n-3)
写出一个采用递归计算过程计算 f 的过程 和 一个采用迭代计算过程计算 f 的过程
递归:recursion
(define (f n)
(cond (< n 3) n)
(>= n 3)
(+ f(- n 1)
(* 2 f(- n 2)
(* 3 f(- n 3))))))
迭代:iteration
(define (ff n)
(+ 2 (* 2 (f 1)) (* 3 (f 0)))..
解题集答案:
递归:
(define (f n) (if (< n 3) n (+ (f (- n 1)) (* 2 (f (- n 2))) (* 3 (f (- n 3))))))
迭代:
要写出函数f 的迭代版本,关键是在于看清初始条件和之后的计算进展之间的关系,
根据函数 f 的初始条件 “如果 n < 3,那么f(n) = n ",有等式
f(0) = 0
f(1) = 1
f(2) = 2
当 n >= ,有f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3)
迭代版函数定义如下:它使用 i 作为渐进下标, n作为最大下标, a、b和c三个变量分别代表函数调用 f (i + 2) 、f(i + 1)、f(i),从f(0) 开始计算
(define (f n)
(f-iter 2 1 0 0 n))
(define (f-iter a b c i n)
(if (= i n)
c
(f-iter (+ a (* 2 b) (* 3 c)) ; new a
a ; new b
b ; new c
(+ i 1)
n)))
效率对比:
递归版本的函数 f 有很多多余的计算,例如需要计算很多次f(1)、f(2)
递归版本的 f 函数是一个指数级复杂度的算法
迭代使用三个变量 f(n - 1), f(n - 2), f(n - 3) 的值,使用自底向上的计算方式进行计算,因此迭代版本没有多余的重复计算工作,它的复杂度正比n,是一个线性迭代函数。
练习 1.12
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
。。。。。
Write a procedure that computes elements of Pascal's triangle by means of a recursive process.
三角形边界的数都是1,内部的每个数是位于它上面的两个数之和。
请写一个过程,采用递归计算过程 计算出帕斯卡三角形的各个元素。
答案:
(define (Pascal n)
(cond (= (/ n 2) 0)
((= n 2)
(define (Pascal x y)
(+ (Pascal (- x 1) (- y 1)) (Pascal (- x 1) y)
解题集:
如果使用 pascal(row, col)
代表第 row
行和第 col
列上的元素的值,可以得出一些性质:
- 每个
pascal(row, col)
由pascal(row-1, col-1)
(左上边的元素)和pascal(row-1, col)
(右上边的元素)组成。 - 当
col
等于0
(最左边元素),或者row
等于col
(最右边元素)时,pascal(row, col)
等于1。
- 每个
比如说,当 row = 3
, col = 1
时, pascal(row,col)
的值为 3
,而这个值又是根据 pascal(3-1, 1-1) = 1
和 pascal(3-1, 1) = 2
计算出来的。
综合以上的两个性质,就可以写出递归版本的 pascal
函数了:
(define (pascal row col) (cond ((> col row) (erroe "unvalid col value")) ((or (= col ) (= row col)) ) () (- col )) (pascal (- row ) col)))))
前面给出的递归版本 pascal
函数的效率并不好,首先,因为它使用的是递归计算方式,所以它的递归深度受限于解释器所允许的最大递归深度。
其次,递归版本 pascal
函数计算值根据的是公式 这个公式带有重复计算,并且复杂度也很高,因此,前面给出的 pascal
函数只能用于 row
和 col
都非常小的情况。
要改进这一状况,可以使用帕斯卡三角形的另一个公式来计算帕斯卡三角形的元素
,其中表示的阶乘(factorial)
阶乘:factorial
(跌代版)
(define (factorial n)
(fact-iter 1 1 n))
(define (fact-iter product counter max-count)
(if (> counter max-count)
product
(fact-iter (* counter product)
(+ counter 1)
max-count)))
跌代版的pascal定义:
(define (pascal row col)
(/ (factorial row)
(* (factorial col)
(factorial (- row col)))))
迭代版的 pascal
函数消除了递归版 pascal
的重复计算,并且它是一个线性复杂度的算法
因此,
迭代版的 pascal
比起递归版的 pascal
要快不少,并且计算的值的大小不受递归栈深度的控制:
1.2.3 增长的阶
目的:描述不同的计算过程在消耗资源的速率上可能存在着巨大差异。
计算阶乘: 计算过程所需步骤的增长 空间需求的增长
递归: O(n) O(n)
迭代: O(n) O(1)(即为一个常数)
斐波那契计算
树形递归 O(黄金分割率的 n 次方) O(n)
练习1.15
a:
p是翻译的定义,限制为(> (abs angle) 0.1))
每递归一次,angle \ 3.0
12.15 \ 3.0
4.05
1.35
0.45
0.15
0.05
五次
b:
在求值(sine a)的时候,a每次都被除以 3.0 , 而sine是一个递归程序,因此它的时间和空间复杂度都是 O(log a)。 (由O(log3(a/0.033333 = log3(30a) = log3(30) + log3(a), log3(30)是常数,因此是O(log a)
即每当 a 增大一倍(准确的说是乘因子 3),p的运行次数就会增加一次。
1.2.4 求幂