5.1.4 使用一个栈来实现递归
正如这个思想所展示的那样,我们能实现任何的迭代的过程,通过指定一个寄存器机器,
让它有一个寄存器来对应于过程的每个状态变量。机器则重复地执行一个控制器的循环,
改变寄存器的内容,直到一些中止的条件被满足。在控制器序列的任何一个点上,机器的
状态(表示迭代过程的状态)是完全取决于寄存器的内容(状态变量的值)。
实现递归过程,然而,需要一种额外的机制。考虑如下的计算斐波那些数的递归方法,
它是我们首先在1.2.1部分中举的例子:
(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1) ) )
)
)
正如从这个程序中看到的,计算N!需要计算(N-1)!。我们GCD机器 以如下的程序建模。
(define (gcd a b)
(if (= b 0)
a
(gcd b (remainder a b))))
它是相似的,它不得不计算另一个GCD。但是在gcd程序和 factorial程序之间,
这有一个重要的区别。gcd程序把原来的计算归结为新的计算,factorial程序
要求另一个factorial作为自己的子问题。在gcd计算中,新的gcd计算的答案就是原来的
gcd计算的答案。为了计算下一个gcd,我们简化了在GCD机器中的输入的寄存器处放置
新的实际参数的位置,并且通过执行相同的控制器序列而重用了机器的数据路径。
当机器解决了最后的GCD问题,它就完成了整个计算。
在斐波那些数的例子中,(或者任何递归的过程)新的斐波那些数的子问题的答案
并不是原来的问题的答案。(n-1)!的值必须乘以n才能得到最终的答案。如果我们
试图使用GCD的设计,解决斐波那些数的子问题通过把n的寄存器的值减一,重运行
斐波那些数的机器时,我们不再有可用的n的原来的值了,这个值需要用来计算最终的
结果。我们所以需要第二个斐波那些数的机器来计算子问题。第二个计算本身有一个子问题,
它需要第三个斐波那些数的机器,等等。因为任何一个机器包含着另一个机器,总机器包含
着相似的机器的无限嵌套,并且因此不能被组装成一个固定的,有限的部分。
然而,如果我们安排使用相同的组件,为了机器的每一个嵌套的实例,我们能以一个寄存器机器
实现斐波那些数的计算过程。特别是,机器计算n的阶乘,应该使用相同的组件来计算
n-1的阶乘 ,n-2的阶乘等等。这是可行的,因为尽管斐波那些数的执行过程显示出为了执行
这个计算需要这个机器上的无数个程序副本,但是在给定的某个时候,这些副本中仅有一个是需要处于活跃的状态中的。当机器遇到了一个递归的子问题时,它能在主问题上暂停工作,重用相同的物理部分来处理子问题,然后再继续做刚才暂停的计算。
在子问题上,寄存器的内容不同于它们在主问题上的内容。(在这个例子中,寄存器n的值是减1的)。为了能够继续被暂停的计算,机器必须保存任何寄存器的值,在子问题被解决之后,它们是被需要的,为了让它们被用来恢复被暂停的计算。在斐波那些数的例子中,我们将保存n的原来
的值,为了当我们完成了子问题后用来恢复被暂停的主问题。
因为在嵌套的递归调用的深度上没有一个先知的限制,我们可能需要保存很多的寄存器的值。
这些值必须被恢复以它们的被保存时的相反的顺序,因为在递归的嵌套中,最后的子问题被
最行解决。这显示了一个栈的使用,或者是“后进,先出”的数据结构,来保存寄存器的值。
通过添加两个类型的指令,我们能扩展寄存器的机器语言来包括栈:值被放在栈上,使用一个
保存指令,从栈上取出用来恢复使用一个恢复的指令。一个值的序列被保存在栈上之后,一个恢复的序列将以相反的顺序检索这些值。
在栈的辅助下,为了任何一个斐波那些数的子问题,我们能重用斐波那些数的
机器的数据路径的一个副本。在重用控制器的序列来操作数据路径方面,有一个
相似的设计问题。为了重执行斐波那些数的计算,控制器不能简单地回溯到开头,
正如 一个迭代的过程,因为解决了(n-1)的阶乘这个子问题后,机器必须还把结果乘以n.
控制器必须暂停它的n的阶乘的计算,解决 (n-1)的阶乘这个子问题,然后再继续 n的阶乘
的计算。斐波那些数的计算的视角建议使用子程序的机制,这在5.1.3部分中有描述,在那里,
有控制器使用一个继续的寄存器来转换序列的部分,解决子问题,然后再继续主问题中的剩下
的部分。我们能因此把一个子程序的返回的入口点保存在一个继续的寄存器中。围绕着任何一个子程序的调用,我们保存和恢复寄存器,正如我们对寄存器n的做法。也就是说,斐波那些数的子程序必须在它调用了子程序后把一个新的值放入继续寄存器,但是将需要旧值为了返回用。
图5.11 显示了实现了递归的斐波那些数的程序的机器的数据路径和控制器。
机器有一个栈和三个寄存器,叫做n,val,continue.为了化简数据路径的图,我们
没有为寄存器赋值的按钮命名,仅为栈操作的按钮命名了(sc 和sn 保存寄存器,rc 和rn恢复寄存器)。为了操作机器,我们把我们期望计算的斐波那些数的结果放在寄存器n中,启动了机器。当
机器到了fact-done,计算完成了,答案在val寄存器中。在控制器序列中,n和continue被保存
在任何的递归调用之前,因为在子程序返回之后,val的旧值没有用了。仅有新值,被子计算生成的是被需要的。尽管在原则上斐波那些数的计算需要一个无限的机器,在图5.11中的机器是实际上有限的,除了栈的部分,它是潜在的无上限的。一个栈的任何特定的物理的实现,然而将是有限的大小,这将限制能被机器处理的递归调用的深度。斐波那些数的实现显示了为了实现递归算法的通用的策略正如普通的寄存器机器用栈来作实际的参数。当遇到了一个递归的子问题时,我们在栈上保存寄存器的值,它的当前的值在子问题被解决后将被使用,解决子问题,然后恢复保存的寄存器,并且继续在主问题上执行。继续的寄存器必须总是被保存。其它的寄存器是否需要被保存依赖于特定的机器,因为并不是所有的递归计算都需要寄存器的原始的值,在子问题的解决过程中寄存器的值被修改了。(见练习5.4)
*一个双递归
让我们检查一个更复杂的递归的过程,斐波那些数的树形递归的计算,它是我们在
1.2.2部分中介绍过的:
(define (fib n)
(cond ((= n 0) 0)
((= n 1 ) 1)
(else (+ (fib (- n 1))
(fib (- n 2))
)
)
)
)
正如斐波那些数,我们能实现递归的斐波那些数计算作为一个寄存器机器
带有寄存器n,val,continue.这个机器是更复杂的,因为在控制器的序列中有两
个地方我们需要执行递归调用,一个是计算fib(n-1),另一个是计算fib(n-2)。
为了安装这些调用中的每一个,我们保存寄存器,它的值在稍后需要用到,
设置n寄存器为我们需要递归计算的(n-1或者n-2),并且给continue赋值为
在主序列中的入口点,这是用来在(afterfib-n-1 或者 afterfib-n-2)的返回时用的。
我们然后到了fib-loop.当我们从递归的调用中返回时,答案在val。图5.12显示了这个机器的
控制器序列。
(controller
(assign continue (label fact-done))
fact-loop
(test (op =) (reg n) (const 1))
(branch (label base-case))
(save continue)
(save n)
(assign n (op -) (reg n) (const 1))
(assign continue (label after-fact))
(goto (label fact-loop))
after-fact
(restore n)
(restore continue)
(assign val (op *) (reg n) (reg val))
(goto (reg continue))
base-case
(assign val (const 1))
(goto (reg continue))
fact-done
)
图5.11 一个递归的斐波那些数的机器
(controller
(assign continue (label fib-done))
fib-loop
(test (op <) (reg n) (const 2))
(branch (label immediate-answer))
(save continue)
(assign continue (labelfib-n-1))
(save n)
(assign n (op -) (reg n) (const 1))
(goto (label fib-loop))
afterfib-n-1
(restore n)
(restore continue)
(assign n (op -) (reg n) (const 2))
(save continue)
(assign continue (label afterfib-n-2))
(save val)
(goto (label fib-loop))
afterfib-n-2
(assign n (reg val))
(restore val)
(restore continue)
(assign val (op +) (reg val) (reg n))
(goto (reg continue))
immediate-answer
(assign val (reg n))
(goto (reg continue))
fib-done
)
图5.12 计算斐波那些数的机器的控制器
练习5.4
指定一个寄存器机器实现如下的程序。对于任何一个机器,
写出它的控制器指令序列和画一个图显示它的数据路径。
a.递归过程的程序如下:
(define (expt b n)
(if (= n 0)
1
(* b (expt b (- n 1)))
)
)
b.迭代过程的程序如下:
(define (expt b n)
(define (expt-iter counter product)
(if (= counter 0)
product
(expt-iter (- counter 1))
))
(expt-iter n 1)
)
练习5.5
手动模拟斐波那些数和斐波那些数的机器,使用一个非正常的输入
(需要至少执行一次递归调用)。在执行的任何一个关键的点上,
显示栈的内容。
练习5.6
苯注意到斐波那些数的机器的控制器序列有一个特殊的保存和恢复指令,
为了让机器运行的更快,它们可以被删除,这些指令在哪?