# 5.1.4使用堆栈实现递归

> 译者： [https://sicp.comp.nus.edu.sg/chapters/99](https://sicp.comp.nus.edu.sg/chapters/99)

到目前为止，我们已经说明了这些思想，我们可以通过指定一个具有与该过程的每个状态变量相对应的寄存器的寄存器机器来实现任何迭代过程。 机器重复执行控制器循环，更改寄存器的内容，直到满足某些终止条件为止。 在控制器序列的每个点上，机器的状态（代表迭代过程的状态）完全由寄存器的内容（状态变量的值）确定。

但是，实施递归过程需要附加的机制。 考虑以下用于计算阶乘的递归方法，我们在 [1.2.1](12) 部分中首先进行了检查：

```js
function factorial(n) {
    return n === 1 
           ? 1
           : n * factorial(n - 1);
}
```

从函数中可以看出，计算![n!](img/9dc6259785da3af5bb9d517f330964af.jpg)需要计算![%28n-1%29!](img/fe43508546d21b783b27a79f8cf2ff3b.jpg)。 我们的GCD机器，基于功能

```js
function gcd(a, b) {
    return b === 0 ? a : gcd(b, a % b);
}
```

similarly had to compute another GCD. But there is an important difference between the `gcd` function, which reduces the original computation to a new GCD computation, and `factorial`, which requires computing another factorial as a subproblem. In GCD, the answer to the new GCD computation is the answer to the original problem. To compute the next GCD, we simply place the new arguments in the input registers of the GCD machine and reuse the machine's data paths by executing the same controller sequence. When the machine is finished solving the final GCD problem, it has completed the entire computation.

在阶乘（或任何递归过程）的情况下，新阶乘子问题的答案不是原始问题的答案。 必须将![%28n-1%29!](img/fe43508546d21b783b27a79f8cf2ff3b.jpg)的值乘以![n](img/493731e423d5db62086d0b8705dda0c8.jpg)才能得到最终答案。 如果我们试图模仿GCD设计，并通过递减`n`寄存器并重新运行阶乘机器来解决阶乘子问题，我们将不再有可用的`n`旧值来乘以结果。 因此，我们需要第二个析因机器来处理子问题。 此第二阶乘计算本身具有一个阶乘子问题，这需要第三阶乘子机，依此类推。 由于每个析因机器中都包含另一个析因机器，因此整个机器包含无数类似机器的嵌套，因此无法由固定的有限数量的零件构成。

但是，如果我们可以安排对机器的每个嵌套实例使用相同的组件，则可以将析因过程实现为注册机器。 具体来说，计算![n!](img/9dc6259785da3af5bb9d517f330964af.jpg)的计算机应使用相同的组件来处理计算![%28n-1%29!](img/fe43508546d21b783b27a79f8cf2ff3b.jpg)的子问题，处理![%28n-2%29!](img/54fffa88b6934c9e2dce61058c39a8f4.jpg)的子问题，依此类推。 这是有可能的，因为尽管析因过程表明需要无限制数量的同一台计算机的副本来执行计算，但是在任何给定时间仅需要激活这些副本之一。 当机器遇到递归子问题时，它可以挂起主要问题的工作，重用相同的物理部件来处理子问题，然后继续挂起的计算。

在子问题中，寄存器的内容将不同于主要问题中的内容。 （在这种情况下，`n`寄存器将递减。）为了能够继续执行暂停的计算，计算机必须保存子问题解决之后将需要的所有寄存器的内容，以便可以恢复这些内容以继续 暂停的计算。 在阶乘的情况下，我们将保存`n`的旧值，以在我们完成对递减的`n`寄存器的阶乘的计算后将其恢复。 [[1]](99#footnote-1)

由于嵌套递归调用的深度没有先验_限制，因此我们可能需要保存任意数量的寄存器值。 必须以与保存顺序相反的顺序还原这些值，因为在一系列递归中，要输入的最后一个子问题是第一个要完成的子问题。 这要求使用_堆栈_或&lt;quote&gt;后进先出&lt;/quote&gt;数据结构来保存寄存器值。 我们可以通过添加两种指令来将寄存器机语言扩展为包括堆栈：使用`save`指令将值放在堆栈上，并使用`restore`指令从堆栈中恢复值。 在堆栈中将`save` d的值序列存储后，`restore` s的序列将以相反的顺序检索这些值。 [[2]](99#footnote-2)_

借助堆栈，我们可以为每个阶乘子问题重用阶乘机器数据路径的单个副本。 在重用操作数据路径的控制器序列中存在类似的设计问题。 为了重新执行阶乘计算，控制器不能像迭代过程那样简单地循环回到开始，因为在解决![%28n-1%29!](img/fe43508546d21b783b27a79f8cf2ff3b.jpg)子问题后，机器仍必须将结果乘以![n](img/493731e423d5db62086d0b8705dda0c8.jpg)。 控制器必须暂停![n!](img/9dc6259785da3af5bb9d517f330964af.jpg)的计算，解决![%28n-1%29!](img/fe43508546d21b783b27a79f8cf2ff3b.jpg)子问题，然后继续![n!](img/9dc6259785da3af5bb9d517f330964af.jpg)的计算。 这种阶乘计算的观点建议使用 [ 5.1.3 ](98)级别&lt;/quote&gt;将使用相同的`continue`寄存器。 也就是说，阶乘子例程在调用子问题时必须在`continue`中放置一个新值，但是它将需要旧值以便返回到调用它来解决子问题的位置。

图 [5.11](99#fig_5.11) 显示了实现递归`factorial`功能的计算机的数据路径和控制器。 该计算机具有一个堆栈和三个寄存器，分别称为`n`，`val`和`continue`。 为了简化数据路径图，我们没有命名寄存器分配按钮，仅命名了堆栈操作按钮（`sc`和`sn`用于保存寄存器，`rc`和`rn`用于还原寄存器）。 要操作机器，我们将希望计算其因子数并启动机器的编号`n`存入寄存器。 当机器到达`fact-done`时，计算完成，并且答案将在`val`寄存器中找到。 在控制器序列中，`n`和`continue`在每次递归调用之前被保存，并在从调用返回时恢复。 通过分支到`continue`中存储的位置来完成从呼叫返回。 机器启动时会初始化`Continue`，以便最后一次返回将到达`fact-done`。 保留阶乘计算结果的`val`寄存器不会在递归调用之前保存，因为`val`的旧内容在子例程返回后无用。 仅需要新的值，即子计算产生的值。

尽管从原理上讲阶乘计算需要一台无限的机器，但图 [5.11](99#fig_5.11)[5.4](99#ex_5.4) ）。

## 双重递归

让我们研究一个更复杂的递归过程，即斐波那契数的树递归计算，我们在 [1.2.2](13) 部分中介绍了该过程：

```js
function fib(n) {
    return n === 0
           ? 0
           : n === 1
             ? 1
             : fib(n - 1) + fib(n - 2);
}
```

与阶乘一样，我们可以将递归斐波纳契计算实现为具有寄存器`n`，`val`和`continue`的寄存器机。 该机器比用于阶乘的机器更复杂，因为在控制器序列中有两个地方需要执行递归调用：一次计算Fib ![%28n-1%29](img/9133d85d4272fedba08cebbbbe4fdfe6.jpg)，一次计算Fib ![%28n-2%29](img/3745832a2c730630689487d85fdeabac.jpg)。 要为这些调用中的每一个设置，我们保存以后需要其值的寄存器，将`n`寄存器设置为我们需要递归计算其Fib的数字（![n-1](img/23175b78d29762e4a6d26ee3e988f9e3.jpg)或![n-2](img/3196eb3e394f3dbe1e776917d63fb450.jpg)），然后分配给[ `continue`返回主序列中的入口点（分别为`afterfib-n-1`或`afterfib-n-2`）。 然后，我们转到`fib-loop`。 当我们从递归调用返回时，答案在`val`中。 图 [ 5.12 ](99#fig_5.12) 显示了此机器的控制器顺序。 

<figure>![](img/6417644b930908b8060f8359cfb3c5f0.jpg)

```js
controller(
    list(      
       assign("continue", label("fact-done")), // set up final return address
       "fact-loop",
       test(list(op("="), reg("n"), constant(1))),
       branch(label("base-case")),
       // Set up for the recursive call by saving "n" and "continue".
       // Set up "continue" so that the computation will continue
       // at "after-fact" when the subroutine returns.
       save("continue"),
       save("n"),
       assign("n", list(op("-"), reg("n"), const(1))),
       assign("continue", label("after-fact")),
       go_to(label("fact-loop")),
       "after-fact",
       restore("n"),
       restore("continue"),
       assign("val", list(op("*"), reg("n"), reg("val"))),
       // "val" now contains n(n-1)!
       go_to(reg("continue")),        // return to caller
       "base-case",
       assign("val", constant(1)),       // base case: val = 1
       go_to(reg("continue")),        // return to caller
       "fact-done"));
```

**[图5.11](99#fig_5.11)** 一种递归析因机。</figure>

<figure>

```js
controller(
    list(      
       assign("continue", label("fib-done")),
       "fib-loop",
       test(list(op("<"), reg("n"), constant(2))),
       branch(label("immediate-answer")),
       // set up to compute Fib(n-1)
       save("continue"),
       assign("continue", label("afterfib-n-1")),
       save("n"),                  // save old value of n
                                   // clobber n to n - 1       
       assign("n", list(op("-"), reg("n"), constant(1))), 
       go_to(label("fib-loop")),   // perform recursive call
       "afterfib-n-1",                                
                                   // upon return, "val" contains Fib(n-1)
       restore("n"),
       restore("continue"),        // set up to compute Fib(n-2)
       assign("n", list(op("-"), reg("n"), constant(2))),
       save("continue"),
       assign("continue", label("afterfib-n-2")),
       save("val"),                // save Fib(n-1)
       go_to(label("fib-loop")),
       "afterfib-n-2",             // upon return, "val" contains Fib(n-2)
       assign("n", reg("val")),    // "n" now contains Fib(n-2)
       restore("val"),             // "val" now contains Fib(n-1)
       restore("continue"),
       assign("val",               // Fib(n-1) + Fib(n-2)
           list(op("+"), reg("val"), reg("n"))),
       go_to(reg("continue")),     // return to caller, answer is in "val"
       "immediate-answer",
       assign("val", reg("n")),    // base case: Fib(n) = n
       go_to(reg("continue")),
       "fib-done"));
```

**[图5.12](99#fig_5.12)** 用于计算机计算的控制器 斐波那契数。</figure>

<exercise>Specify register machines that implement each of the following functions. For each machine, write a controller instruction sequence and draw a diagram showing the data paths.

1.  递归指数：

    ```js
    function expt(b, n) {
        return n === 0
            ? 1
            : b * expt(b, n - 1);
    }
    ```

2.  迭代指数：

    ```js
    function expt(b, n) {	  
        function expt_iter(counter, product) {
            return counter === 0
                ? product
                : expt_iter(counter - 1, b * product);
        }
        return expt_iter(n, 1);
    }
    ```

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_99_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Hand-simulate the factorial and Fibonacci machines, using some nontrivial input (requiring execution of at least one recursive call). Show the contents of the stack at each significant point in the execution.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_99_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>Ben Bitdiddle observes that the Fibonacci machine's controller sequence has an extra `save` and an extra `restore`, which can be removed to make a faster machine. Where are these instructions?<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_99_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise>

* * *

[[1]](99#footnote-link-1) One might argue that we don't need to save the old `n`; after we decrement it and solve the subproblem, we could simply increment it to recover the old value. Although this strategy works for factorial, it cannot work in general, since the old value of a register cannot always be computed from the new one.

[[2]](99#footnote-link-2) In section <ref name="sec:storage-allocation">[5.3](106)</ref> we will see how to implement a stack in terms of more primitive operations.

