1.2.1 线性递归与迭代

1.2.1 线性递归与迭代

图1.3 计算6!的线性递归的流程
我们开始考虑factorial 函数,定义如下:

有很多的方式计算factorial。一个方式是利用公式 N!=N*(N-1)!对于任何正整数N。

因此,我们计算N! 通过计算(N-1)! 和 它的结果乘以N。如果我们加上规定(约定)
1!=1, 这个观察所得能被直接翻译成一个程序:
(define (factorial n)
   (if (= n 1)
     1
     (* n (factorial (- n 1) ) )
   )
)
我们能够使用1.1.5部分中的替换模型来观察这个程序在计算6!的行为。
正如图1.3中所示。

现在,让我们用另一个不同的角度来看计算factorial。通过规定我们首先
计算1*2,然后结果*3,然后*4等等,直到我们到达n的方式,我们能够描述
一种计算n!的规则。更形式化的说,我们维护着一个运行着的产品,带有一个计数器,
计数器从一数到n.根据规则,我们能够说计数器与产品在从一个步骤到下一个步骤时,
同时地改变着,这就是我们描述的计算。
product=counter*product
counter=counter+1
并且我们规定N!=当计数器到达N时的产品的值。
图1.4 计算6!的线性迭代的流程
一旦重复,我们能够重映射我们的描述,作为一个计算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)
     )
)
正如以前,我们能够使用替换模型,来可视化计算6!的流程。
正如图1.4所示。

比较这两个流程,从视图的一点看,它们似乎几乎没有不同。在相同的域,
两者都计算相同的数学函数,并且任何一个都要求一些到N的步骤来计算N!。
实际上,甚至都执行了乘法的相同序列,得到了部分中间结果的相同序列。
另一方面,当我们考虑两个流程的形状时,我们发现它们的进化过程完全不同。

考虑第一个流程,替换模型揭示出了抽取过程的扩展的形状,在图1.3中由箭头显示。
在流程构建引用操作的链条时扩展发生了,(在这个案例中,是乘法的链条)。
当操作被实际执行时,收缩发生了。这种类型的流程,由引用操作的链条为特征,
叫做递归流程。执行这种流程要求解释器追踪稍后要执行的操作。在N!的计算中,
引用乘法的链条的长度,与N线性增长,因此需要追踪这个值,正如执行的步骤数,
这种的流程被叫做线性递归流程。

相反的是,第二个流程没有增长与收缩,在任何一步,对于任何的N,我们需要追踪的是
产品,计数器,最大计数值这三个变量的当前值。我们叫这种流程为迭代流程。
通常情况下,一个迭代流程,它的状态能被总结成有限个数的一些状态变量,
结合一些规则来描述当流程从一个状态来到下一个状态时,状态变量们应用如何被更新,
一个可选的终止测试规定了流程应该在什么条件下终止。在计算N!时,计算的步骤与N是线性关系,
这种的流程被称为线性迭代流程。

在另外的一种方式中我们也能看到这两种流程的相反。在迭代的案例中,程序的变量
在任何一点处都提供了流程的状态的完整的描述。如果我们在某步骤时停下来,
当我们需要恢复计算时我们需要做的所有的事就是提供解释器需要的这三个程序变量的值。
递归流程不是这样的。在这个案例中有一些附加的隐藏信息,是被解释器维护的,没有
包含在程序变量中,这些隐藏的信息指示出流程处于引用操作链条的具体位置。
链条越长,需要维护的信息就越多。

在迭代与递归的相反方面,我们必须仔细地不被有递归程序标识法的递归流程的标识法
所迷惑。当我们描述一个程序是递归的,我们正引用的人造的事实是程序定义上
(或者直接或者间接地)引用了程序的本身。但是当我们描述一个流程遵循一个模式时
比如说线性递归,我们正在说的是流程如何进化,而不是关于程序如何写的语法的事。
这似乎干扰了我们引用的一个递归程序 用一个迭代的流程生成,成为一个事实上的迭代。
然而,流程真实地是迭代的。它的状态被它的三个状态变量完整地捕捉到了,
并且解释器为了执行流程,只需要追踪这三个状态变量即可。

一个原因是流程与程序之间的区别被混淆的情况是这样的,普通的计算机语言
(如ADA,PASCAL,C)等的绝大部分的实现被设计成这样的方式,这方式是
任何一个递归的程序的解释消耗了一定量的随着程序调用的数量的增长而增加的内存
甚至当程序在原则上被迭代地描述时,也是如此。结果是,这些语言能够描述的迭代流程
仅仅是为了特定目的的循环结构,例如do,repeat,until,for ,while.
我们在第五章中研究的scheme的实现 并没有分享这种情形。它将在恒定的空间内执行
迭代流程,即使它的迭代流程是被一个递归的程序描述的。具有这种属性的实现被叫做
尾递归。在具有尾递归的实现中,使用普通的程序调用机制,迭代就能被表达出来,
所以特定的迭代的写法仅在作为语法糖时是有用的。

练习1.9
如下的两个程序都定义了两个正整数相加的方法,它们都使用了加一(inc)
和减一(dec)的函数.
(define (+ a b)
   (if (= a 0)
        b
  (inc (+ (dec a) b))))

(define (+ a b)
      (if (= a 0)
        b
 (+ (dec a) (inc b))))
使用替换模型,演示在执行(+ 4 5)过程中,上述两个程序生成的执行过程。
这些过程是迭代的还是递归的?

练习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)
当A是上述的程序时,考虑如下的程序:
(define (f n) (A 0 n))

(define (g n) (A 1 n))

(define (h n)  (A 2 n))

(define (k n)  (* 5 n n ))
对于正整数n来说,想一想 f g h程序,定义的精确的数学公式。
例如(k n)计算的是 5N2.

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/81384750