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。

这一节的练习感觉不想写,所以就算了。

 

转载于:https://www.cnblogs.com/mhkds/p/6923669.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值