1.2.2 树形递归

1.2.2 树形递归
计算的另一个公共的模式是树形递归。作为一个例子,考虑一下计算斐波那歇的数的序列。
任何一个数都是前两个数的和。

0,1,1,2,3,5,8,13,21,。。。。

通常,斐波那歇的数能被用如下规则定义:
Fib(n)= 0  如果 n=0
Fib(n)= 1  如果 n=1
Fib(n)= Fib(n-1)+Fib(n-2)  如果 n〉1

为了计算斐波那歇的数,我们能够马上把这个定义翻译成一个递归的程序:
(define (fib n)
   (cond ((= n 0) 0)
         ((= n 1 ) 1)
  (else (+ (fib (- n 1))
           (fib (- n 2))
                )
          )
    )
)
在图1.5中,展示了用树形递归流程生成的计算(fib 5).
考虑这个计算模型,为了计算(fib 5),我们要计算
(fib 4)和 (fib 3).为了计算(fib 4),我们要计算
(fib 3) (fib 2)。总之,进化过程看起来像一棵树。如图1。5所示。
注意在任何一级的分支被分成了两个,除了最底层的。
这反映出一个事实是 FIB函数每次调用自身两次。

这个程序很经典地展示出一个树形的递归, 但是对于计算裴波那数列
是一种不好的方式,因为它有如此多的冗余计算。在图1。5中,(fib 3)
的计算几乎有一半是重复的。在事实上,很容易显示出计算(fib 1) 和(fib 2)的
程序执行了多少次。(即上述树的叶子的数量)为了知道这有多么糟糕,能看到Fiber(N)
的值,随着增长而指数级的增长。更精确的计算(见练习1。13)Fib(N)的整数值接近于

此处数学公未写

因此,流程执行的次数是随着输入的增长而指数级增长的。另一方面,
空间的增长是线性增长的。因为我们仅需要跟踪树中正在计算中的结点。总之,
一个树的递归流程需要的执行次数是正比于树中的结点数,空间的需要是正比于树的最大的深度。

对于斐波那歇的数的计算,我们也能有迭代的公式来计算。想法是使用一个有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))
    )
)

计算fib(n)的第二种方法是一种线性的迭代。两种方法要求的步骤的数量差异
一个是n,一个是像fib(n)本身一样快。 这种差异是巨大的,即使输入很小的情况。

不包括树形递归的过程是可以没有实际用处的。当我们考虑处理在层次结构数据而不是
数字的操作时,我们会发现树递归是一个自然的并且强有力的工具。然而,在数值处理
时,树形递归也是很有用的,它在帮助我们理解和设计程序。例如,尽管第一个fib程序
不如第二个有效率,但是它是更符合人们思维的,更容易写成斐波那歇的数列的lisp语言。
为了写成迭代算法,需要注意的是计算被重映射成迭代过程并且有三个时态变量。

@ 兑换零钱的例子

得到迭代的斐波那歇的算法,仅是一点小聪明。相反的,考虑一下如下的问题。把我们手中的
一美元换成半美元,四分之一美元,一角,五分,一分美元有多少种方法?更通用的是,
我们能够写一个程序计算把一定数量的钱换成零钱有多少种方法吗?

作为递归的流程来看,它有很简单的解决方案。假定我们认为钱币的可用的种类以一定的顺序排放。
然后,有如下的关系存在:

兑换金额为a,没有使用第一种钱币时的兑换方法数 加上
兑换金额为a-d,使用所有种类的钱币时的兑换方法数。d是第一种钱币的面值。

看一看为什么这是对的,注意一下,兑换的方法被分成了两类,没有使用第一种钱币和使用了的。
所以总数等于没有使用的兑换数加上使用了的兑换数。但是后者等于使用了一枚第一种钱币时的兑换数。

因此,我们能够递归地把兑换一定数量的钱的问题,归结为兑换更少的钱和使用更少的钱币种类的问题。
认真地考虑一下这种归结的规则,如果我们能把它分成如下的几类情况,我们能描述这个算法。

 如果A等于0,我们说有一种方式换钱。
 如果A小于0,我们说有零种方式换钱。
 如果N等于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 (- 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)
         )
)  

first-denomination程序以有多少种硬币为参数,返回第一种硬币的面额。
这里我们以硬币从大到小的顺序排列。当然了其它的顺序也是可以的。
现在我们能回答最初的关于1美元的兑换问题了。

(count-change 100)
292

count-change生成了一个树形的递归的过程。冗余也和我们的第一个fib的实现是相似的。另一方面,
对于这个计算如何设计一个更好的算法是不明显的。我们留下这个问题作为一个挑战。值得注意的是,
一个树形递归可能是很低效率的,但是常常容易写和理解。让人们认为通过设计一个小型的编译器,
把树递归转译成更高效率的程序,来计算出相同的结果。
          
练习1.11
一个函数f被定义为 如果 N<3, f(n)=n,如果 N>=3,f(n)=f(n-1)+2(fn-2)+3f(n-3).
通过使用递归的方式,写一个程序计算f.
通过使用迭代的方式,写一个程序计算f.

练习1.12
如下的模式的数字被叫做杨辉三角形

             1
           1   1
         1   2   1
       1   3   3   1
     1   4   6   4   1
        。。。。。
这个三角形的边都是数字1,三角形内部的数字等于它上面的两个数字的和。
写一个程序以递归的方式,计算这个三角形。


练习1.13
证明Fib(n)是最接近 the(n)*sqrt-root(5),the=(1+sqrt-root(5))/2.提示:
 使用推导和斐波那些数的定义,证明fib(n)=()/sqrt-root(5).

猜你喜欢

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