SICP读书笔记(二)
1.2 过程与它们所产生的计算
一个过程也就是一种模式,它描述了一个计算过程的局部演化方式,描述了一个计算过程中的每一个步骤如何基于前面的步骤建立起来。这一节中,我们要考察一些简单过程所产生计算过程的“形状”,还将研究这些计算过程消耗各种重要计算资源(时间和空间)的速率。
1.2.1 线性的递归和迭代
考虑如何计算一个阶乘函数,有一个很简单的方法:
(define (factorial n) (if (= 1 n) 1 (* n (factorial (- n 1)))))
我们可以利用代换模型来观察这个过程在计算,例如6!,时表现出的行为。
我们还有另一种不同的观点可以用来计算阶乘。 我们可以通过维护一个变动的乘积product,以及一个从1到n的计数器counter来帮助我们进行计算。代码如下:
(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)))
与前面一样,我们也可以用替换模型来查看6!的计算过程。
现在比较一下这两种计算。从一个角度看,它们并没有多大差异。它们计算的都是阶乘函数,都需要与n正比的步骤数目,甚至采用了同样的乘运算序列。但是如果我们考虑这两个计算的“形状”,就会发现它们的进展方式大不相同。
对于第一个计算过程,代换模型揭示出一种先展开后收缩的形状。在展开阶段里,这一计算过程构造起一个推迟进行的操作形成的链条,收缩阶段表现为这些运算的实际执行。这种类型的计算过程由一个推迟执行的运算链条刻画,我们称为一个递归计算过程。为了执行这种计算过程,解释器需要维护好那些以后要执行的操作的轨迹。在计算阶乘n!时,我们需要保存的信息量随n线性增长,这样的一个递归计算过程称为一个线性递归过程。
与之对应,第二个计算过程里并没有增长和收缩,我们需要保存的所有东西就只有product、counter、max-count,我们称这种过程为一个迭代计算过程。一般来说,迭代过程是状态可以用固定数目的状态变量描述的过程,与此同时,存在着一套固定的规则来描述状态转移时变量的更新方式。还有一个结束检测,来描述这个计算过程的终止条件。计算n!时,所需步骤随n线性增长,我们称这种过程为线性迭代过程。
我们需要注意不要搞混了递归计算过程和递归过程这两个概念。后者描述的是一个语法形式上的事,前者则是在描述计算过程的模式。
练习 1.9
对于第一个过程
(+ 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
对于第二个过程
(+ 4 5) (+ 3 6) (+ 2 7) (+ 1 8) (+ 0 9) 9
显然第一个计算过程是递归的,第二个计算过程是迭代的。
练习 1.10
显然,A(1,10)=1024,A(2,4)=2^16=65535,A(3,3)=65536
f(n)=2n
g(n)=2^n
h(n)=2^h(n-1), h(1)=2
1.2.2 树形递归
树形递归是另一种常见的计算模式。 考虑一个计算斐波那契数的递归过程:
(define (fib n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (fib (- n 1)) (fib (- n 2))))))
考虑这一计算的模式, 容易想象,它将会展开为一棵树。 这个过程作为树形递归有教育一一四,但是它是一种糟糕的计算斐波那契数的方法,因为它作了太多次冗余计算。事实上,它的计算步骤将随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))))
显而易见,这个方法是一个线性迭代,它的效率比前一种过程高的多。但是我们也不该说,树形递归计算过程没有用。因为当我们考虑在层次结构的数据上操作时,树形递归计算过程会成为自然而威力强大的工具。即使是对于数的计算,树形递归计算过程也能帮助我们理解和设计程序。
实例:换零钱方式的统计
考虑这样一个问题:给了半美元,四分之一美元,10美分,5美分和1美分的硬币,将1美元换成零钱,一共有几种方式?
采用递归过程,我们能很容易地解决这个问题。我们首先将可用的硬币类型按某种顺序排列,那么将总数为a的现金换成n种不同的硬币的不同方式的数目等于:
- 将现金a换成除第一种硬币外的硬币,有多少种可能,加上
- 将a-d换成这些硬币有多少种可能,其中d为第一种硬币的币值
我们很容易便能写出一个递归过程:
(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 (- kinds-of-coins 1)) (cc (- amount (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)))
count-change也会产生一个树形的递归计算过程,其中也有不少冗余的计算,不过我们也可以利用迭代的方法对这种冗余计算进行优化。
练习 1.11
递归过程:
(define (f n) (if (< n 3) n (+ (f (- n 1)) (* 2 (f (- n 2))) (* 3 (f (- n 3))))))
迭代过程:
(define (f n) (f-iter 2 1 0 n)) (define (f-iter a b c count) (if (= count 0) c (f-iter (+ a (* 2 b) (* 3 c)) a b (- count 1))))
练习 1.12
很容易,故略
练习 1.13
很容易,故略去。
1.2.3 增长的阶
本节的内容在诸多算法教材中皆有阐述,故在此不多赘述。
练习 1.14
不难得到,空间的阶为O(n),步骤的阶为O(n5)
练习 1.15
(a)5次
(b)空间和步数均为O(log(a))
1.2.4 求幂
这一节介绍了一个简单的求快速幂的方法,不多赘述。
练习 1.16
代码如下:
(define (fast-expt b n) (fe-iter 1 b n)) (define (fe-iter a b n) (cond ((= n 0) a) ((= n 1) (* a b)) ((even? n) (fe-iter a (* b b) (/ n 2))) (else (fe-iter (* a b) b (- n 1))))) (define (even? n) (= (remainder n 2) 0))
练习 1.17
(define (fast-mul a b) (cond ((= b 0) 0) ((= b 1) a) ((even? b) (fast-mul (double a) (halve b))) (else (+ a (fast-mul a (- b 1))))))
练习 1.18
(define (fast-mul a b) (fm-iter 0 a b)) (define (fm-iter m a b) (cond ((= b 0) m) ((= b 1) (+ m a)) ((even? b) (fm-iter m (double a) (halve b))) (else (fm-iter (+ m a) a (- b 1)))))
练习 1.19
这是一道利用矩阵快速幂求斐波那契数列的题,题目本身十分容易,故略去。
1.2.5 最大公约数
找出两个数的最大公约数(GCD)的方法是著名的欧几里得算法,它基于这样的观察:如果r是a除以b的余数,则a和b的公约数与b和r的公约数相同。
容易将欧几里得算法写成一个过程:
(define (gcd a b) (if (= b 0) a (gcd b (remainder a b))))
欧几里得算法所需的步数是对数增长的,这件事由下面的定理得出:
如果欧几里得算法用k步算出一对整数的gcd,那么这对数中较小的那个数必然大于或等于第k个斐波那契数。
练习 1.20
如果利用正则序我们计算remainder函数的次数将会是应用序的两倍,分别在检测b是否为0时以及递归触底之后各需求值一次,而应用序则只需在递归触底时求值。
1.2.6 实例:素数检测
本节将描述两种检查一个整数是否为素数的方法。
寻找因子
一种直接的方法是,从2测试到√n为止,这里不多赘述
费马检查
费马检查基于著名的费马小定理,我们通过随机取a,并计算a的n次方模n的值,如果与a模n同余,我们就认为n有较大的几率为素数。这里计算n次方并取模可以使用前面介绍的快速求幂的方法。
概率方法
费马检查和我们前面已经熟悉的算法不同,它的结果只有概率上的正确性,一个数通过了费马检查只能作为它是素数的一个很强的证据,并不保证n一定是素数。我们希望,对于任何一个数,如果执行检查的次数足够多,且n通过了检查,那么就能是这个检查出错的概率减小到所需要的任意程度。
不幸的是,这一断言并不完全正确。确实存在着一些能够骗过费马检查的n,它对于任意a<n都能通过费马检查。由于这种数极其罕见,费马检查在实践中还是很可靠的。
练习 1.21
最小因子分别为 199, 1999, 7。
这一节的练习感觉不想写,所以就算了。