一、线性的递归和迭代
首先考虑由下面表达式定义的阶乘函数:
n
!
=
n
⋅
(
n
−
1
)
⋅
(
n
−
2
)
…
…
3
⋅
2
⋅
1
n! = n \cdot (n-1) \cdot (n-2) ……3 \cdot 2 \cdot 1
n!=n⋅(n−1)⋅(n−2)……3⋅2⋅1
计算阶乘的方式有许多种,一种最简单方式就是利用下述认识:对于一个正整数
n
n
n,
n
!
n!
n!就等于
n
n
n乘以
(
n
−
1
)
!
(n-1)!
(n−1)!:
n
!
=
n
⋅
[
(
n
−
1
)
⋅
(
n
−
2
)
…
…
3
⋅
2
⋅
1
]
=
n
⋅
(
n
−
1
)
!
n! = n \cdot [(n-1) \cdot (n-2) ……3 \cdot 2 \cdot 1] = n \cdot (n-1)!
n!=n⋅[(n−1)⋅(n−2)……3⋅2⋅1]=n⋅(n−1)!
基于以上认识我们就可以写出一个过程来实现计算阶乘了:
(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1)))))
尝试把上述过程直译为中文:
(定义 (阶乘 元)
(if (= 元 1)
1
(* 元 (阶乘 (- 元 1)))))
下图展示了计算
6
!
6!
6! 时所表现出的行为:
现在让我们采用另一种不同的观点来计算阶乘。我们可以将计算阶乘n!的规则描述为:
先乘起1和2,而后将得到的结果乘以3,而后再乘以4,这样下去直到达到n。更形式地说,我们要维持着一个变动中的乘积product,以及一个从1到n的计数器counter,这一计算过程可以描述为counter和product的如下变化,从一步到下一步,它们都按照下面规则改变:
product ← counter · product
counter ← counter + 1
可以看到,n! 也就是计数器counter超过n时乘积product的值。
我们又可以将这一描述重构为一个计算阶乘的过程:
(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)))
尝试把上述过程直译为中文:
(定义 (阶乘 元)
(连乘 1 1 元))
(定义 (连乘 结果 统计个数 所需个数)
(如果(> 统计个数 所需个数)
结果
(连乘 (* 结果 统计个数)
(+ 统计个数 1)
所需个数)))
可以应用替换模型来查看
6
!
6!
6! 的计算过程,如图:
线性递归运行时是不能终止的,运行中终止只能重新进行计算;而线性迭代,则可以通过得知程序终止那点的参数,重新继续计算。这是线性迭代与线性递归特点上的一大区别。
线性递归运行时如果要运算的数目较大,需要维护的数据就可能超多,需要占用的资源就可能特别大。
二、树形递归
1.斐波那契数列计算
现在考虑斐波那契数列的计算,斐波那契数列由下面的规则定义:
F
i
b
(
n
)
=
{
0
如
果
n
=
0
1
如
果
n
=
1
F
i
b
(
n
−
1
)
+
F
i
b
(
n
−
2
)
否
则
Fib(n)=\left\{ \begin{array}{rcl} 0 & &如果 n = 0\\ 1 & & 如果 n = 1 \\ Fib(n-1) + Fib(n-2) & & 否则 \end{array} \right.
Fib(n)=⎩⎨⎧01Fib(n−1)+Fib(n−2)如果n=0如果n=1否则
即每个数都是前两个数之和,最前面两个数分别是0和1:
0
,
1
,
1
,
2
,
3
,
5
,
8
,
13
,
21
,
…
…
0, 1, 1, 2, 3, 5, 8, 13, 21, ……
0,1,1,2,3,5,8,13,21,……
可以很快将上面的定义翻译为一个计算斐波那契数的递归过程:
(define (fib n)
(cond ((= n 0) 0)
((= n 1) 1)
(else (+ (fib (- n 1))
(fib (- n 2))))))
尝试将上述过程直译为汉语
(定义 (斐波那契数列 位)
(情况符合 ((= 位 0) 0)
((= 位 1) 1)
(否则 (+ (斐波那契数列 (- 位 1))
(斐波那契数列 (- 位 2))))))
考虑这一计算的模式。为了计算(fib 5), 我们需要计算出(fib 4) 和(fib 3)。而为了计算(fib 4), 又需要计算(fib 3)和(fib 2)。 一般而言,这一展开过程看起来像一棵树,如图所示:
以上这种写法虽然直观,但性能却并不好。
我们可以规划出一种计算斐波那契数的迭代计算过程,其基本想法就是用一对整数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))))
尝试把上述过程直译为汉语:
(定义 (斐波那契数列 位)
(循环求取前两位和 1 0 位))
(定义 (循环求取前两位和 甲 乙 个数)
(如果 (= 个数 0)
乙
(循环求取前两位和 (+ 甲 乙) 甲 (- 个数 1))))
2.实例:换零钱方式的统计
现在考虑下面的问题:
给了半美元、四分之一美元、10美分、5美分和1美分的硬币,将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
(- 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)))
尝试把上述过程直译为汉语:
(定义 (零钱兑换有几种方式 金额)
(兑换零钱有几种方式 金额 5))
(定义 (兑换零钱有几种方式 金额 零钱种类)
(情况符合 ((= 金额 0) 1)
((或 (< 金额 0) (= 零钱种类 0)) 0)
(其它情况 (+ (兑换零钱有几种方式 金额
(- 零钱种类 1))
(兑换零钱有几种方式 (- 金额 (最后一种零钱的面值是多少 零钱种类))
零钱种类))))))
(定义 (最后一种零钱的面值是多少 零钱种类)
(情况符合 ((= 零钱种类 1) 1)
((= 零钱种类 2) 5)
((= 零钱种类 3) 10)
((= 零钱种类 4) 25)
((= 零钱种类 5) 50)))
练习1:
函数f由如下的规则定义:如果n<3,那么f(n)=n;如果n≥3,那么f(n)=f(n-1) +2f(n-2)+3f(n-3)。请写一个采用递归计算过程计算f的过程。再写一个采用迭代计算过程计算f的过程。
解 :可以如此表示题中函数:
f
(
n
)
=
{
n
如
果
n
<
3
f
(
n
−
1
)
+
2
f
(
n
−
2
)
+
3
f
(
n
−
3
)
如
果
n
≥
3
f(n)=\left\{ \begin{array}{rcl} n & &如果 n < 3\\ f(n-1) + 2f(n-2)+3f(n-3) & & 如果n \geq 3 \end{array} \right.
f(n)={nf(n−1)+2f(n−2)+3f(n−3)如果n<3如果n≥3
由此可以写出递归求解的过程:
(define (f n)
(cond ((< n 3) n)
((>= n 3) (+ (f (- n 1))
(* 2 (f (- n 2)))
(* 3 (f (- n 3)))))))
尝试把上述过程直译为汉语:
(定义 (道 元)
(情况符合 ((< 元 3) 元)
((>= 元 3) (+ (道 (- 元 1))
(* 2 (道 (- 元 2)))
(* 3 (道 (- 元 3)))))))
可以先写下计算过程,来帮助分析迭代法版本的编程:
f ( 0 ) = 0 f ( 1 ) = 1 f ( 2 ) = 2 f ( 3 ) = f ( 2 ) + 2 f ( 1 ) + 3 f ( 0 ) f ( 4 ) = f ( 3 ) + 2 f ( 2 ) + 3 f ( 1 ) \begin{array}{lcr} f(0) = 0 \\ f(1) = 1 \\ f(2) = 2 \\ f(3) = f(2) + 2f(1) + 3f(0) \\ f(4) = f(3) + 2f(2) + 3f(1) \end{array} f(0)=0f(1)=1f(2)=2f(3)=f(2)+2f(1)+3f(0)f(4)=f(3)+2f(2)+3f(1)
接下来,迭代法版本代码:
(define (f n)
(f-iter 0 1 2 0 n))
(define (f-iter a b c counter n)
(if (= counter n)
a
(f-iter b
c
(+ c (* 2 b)(* 3 a))
(+ counter 1)
n)))
尝试把上述过程直译为中文:
(定义 (道 元)
(迭代法求道 0 1 2 0 元))
(定义 (迭代法求道 甲 乙 丙 位 元)
(如果 (= 位 元)
甲
(迭代法求道 乙
丙
(+ 丙 (* 2 乙)(* 3 甲))
(+ 位 1)
元)))
练习2:
下面数值模式称为杨辉三角:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
...
三角形边界上的数都是1,内部的每个数是位于它上面的两个数之和3。请写一个过程,它采用递归计算过程计算出杨辉三角。
解 :
先简单用代入数值的方法来描述一下杨辉三角形就可以帮助编出递归版本的过程:
杨辉三角的第一行第一个数是1;
第二行第一个数是1,第二行第二个数是1;
第三行第一个数是1,第三行第三个数是1,第三行第二个数等于第二行第一个数加上第二行第二个数;
第四行第一个数是1,第四行第四个数是1,第四行第二个数等于第三行第一个数加上第三行第二个数,第四行第三个数等于第三行第二个数加第三行第三个数;
第五行第一个数等于1,第五行第五个数等于1,第五行第二个数等于第四行第一个数加上第四行第二个数,第五行第三个数等于第五行第二个数加上第三个数,第五行第四个数等于第四行第三个数加上第四行第四个数;
……
接下来尝试把上面的描述翻译为程序过程:
(define (pascal-triangle row col)
(cond ((< row col) -1)
((or (= col 1) (= col row)) 1)
(else (+ (pascal-triangle (- row 1) (- col 1))
(pascal-triangle (- row 1) col)))))
尝试把上面的过程直译为汉语:
(定义 (杨辉三角 行 列)
(情况符合 ((< 行 列) -1)
((|| (= 行 1) (= 行 列)) 1)
(其它情况 (+ (杨辉三角 (- 行 1)(- 列 1))
(杨辉三角 (- 行 1) 列)))))
参考文献:
[1] [美]Julie Sussman.计算机程序的构造和解释[M]. 裘宗燕译注.北京:机械工业出版社,1996.