这是将任何递归过程转换为尾递归的通用方法吗?


13

看来我已经找到了将任何递归过程转换为尾递归的通用方法:

  1. 使用额外的“结果”参数定义帮助程序子过程。
  2. 将应用于过程的返回值的参数应用于该参数。
  3. 调用此帮助程序过程即可开始。“结果”参数的初始值是递归过程的退出点的值,因此,最终的迭代过程将从递归过程开始收缩的地方开始。

例如,这是要转换的原始递归过程(SICP练习1.17):

(define (fast-multiply a b)
  (define (double num)
    (* num 2))
  (define (half num)
    (/ num 2))
  (cond ((= b 0) 0)
        ((even? b) (double (fast-multiply a (half b))))
        (else (+ (fast-multiply a (- b 1)) a))))

这是转换后的尾递归过程(SICP练习1.18):

(define (fast-multiply a b)
  (define (double n)
    (* n 2))
  (define (half n)
    (/ n 2))
  (define (multi-iter a b product)
    (cond ((= b 0) product)
          ((even? b) (multi-iter a (half b) (double product)))
          (else (multi-iter a (- b 1) (+ product a)))))
  (multi-iter a b 0))

有人可以证明还是反对?


1
首先考虑:这可能适用于所有单个递归函数,但是如果它适用于进行多个递归调用的函数,我会感到惊讶,因为这暗示着,例如,您可以在不需要堆栈的情况下实现quicksort 空间。(快速排序的现有有效实现通常在堆栈上进行1次递归调用,并将其他递归调用转换为可以(手动或自动)转换为循环的尾调用。)O(logn)
j_random_hacker

再想一想:选择b2的幂表示初始设置product为0并不完全正确。但是在b奇数时将其更改为1无效。也许您需要2个不同的累加器参数?
j_random_hacker 2016年

3
您尚未真正定义非尾递归定义的转换,添加一些结果参数并将其用于累加非常模糊,并且几乎不能推广到更复杂的情况,例如,您有两次递归调用的树遍历。但是,存在一个更精确的“继续”概念,您可以在其中进行部分工作,然后让“继续”功能接管您的工作,并将当前完成的工作作为参数。这称为连续传递样式(cps),请参见en.wikipedia.org/wiki/Continuation-passing_style
Ariel

4
这些幻灯片fsl.cs.illinois.edu/images/d/d5/CS422-Fall-2006-13.pdf包含cps转换的描述,您可以在其中进行一些任意表达式(可能带有非尾调用的函数定义)并将其转换为只包含尾部调用的等效表达式。
Ariel

@j_random_hacker是的,我可以看到我的“转换”过程实际上是错误的……
nalzok

Answers:


12

您对算法的描述在这一点上实在太模糊而无法评估。但是,这里有些事情要考虑。

CPS

实际上,有一种方法可以将任何代码转换为仅使用尾调用的形式。这是CPS转换。CPS(连续传递样式)是通过向每个函数传递延续来表达代码的一种形式。延续是代表“计算的其余部分”的抽象概念。在CPS形式表示代码,以自然的方式具体化的延续是作为一个接受值的函数。在CPS中,不是将函数返回值,而是将表示当前延续的函数应用于该函数“返回”的值。

例如,考虑以下功能:

(lambda (a b c d)
  (+ (- a b) (* c d)))

可以在CPS中表示如下:

(lambda (k a b c d)
  (- (lambda (v1)
       (* (lambda (v2)
            (+ k v1 v2))
          a b))
     c d))

它很丑陋,而且通常很慢,但是确实具有某些优点:

  • 转换可以完全自动化。因此,无需以CPS形式编写(或查看)代码。
  • 结合使用thunk和trampapping,可以将其用于不提供尾部调用优化的语言中的尾部调用优化。(直接尾部递归函数的尾部调用优化可以通过其他方法来完成,例如将递归调用转换为循环。但是间接递归以这种方式转换并不容易。)
  • 使用CPS,连续性成为一流的对象。由于连续性是控制的本质,因此几乎可以将任何控制操作员实现为库,而无需该语言的任何特殊支持。例如,goto,异常和协作线程都可以使用延续进行建模。

总拥有成本

在我看来,与尾部递归(或通常称为尾部调用)有关的唯一原因是出于尾部调用优化(TCO)的目的。因此,我认为有一个更好的问题是“我的转换收益代码是否可以优化尾调?”。

如果我们再次考虑CPS,它的特征之一就是CPS中表示的代码仅由尾部调用组成。由于所有操作都是尾部调用,因此我们不需要将返回点保存到堆栈中。因此,所有CPS格式的代码都必须进行尾调用优化,对吗?

好吧,不完全是。您会看到,虽然看起来我们已经消除了堆栈,但我们所做的只是改变我们表示堆栈的方式。现在,堆栈是表示继续的闭包的一部分。因此,CPS不会神奇地优化我们所有的代码尾部调用。

因此,如果CPS无法将所有内容都设为TCO,那么是否存在专门针对直接递归的转换呢?不,不是一般。有些递归是线性的,但有些则不是。非线性(例如,树)递归仅必须在某处保持可变数量的状态。


在“ TCO ”小节中,当您说“优化尾调用”时,您的意思实际上是“具有恒定的内存使用量”,这有点令人困惑。动态内存使用率不是恒定的,这仍然不能否认以下事实:调用确实是尾部的,并且堆栈使用率没有无限增长。SICP称此类计算为“迭代式”,因此说“尽管是TCO,但仍然没有使其迭代”(对我而言)可能是一个更好的措辞。
尼斯将于

@WillNess我们仍然有一个调用堆栈,只是表示方式有所不同。结构不会因为我们使用堆而不是硬件堆栈而改变。毕竟,有很多基于动态堆内存的数据结构,它们的名称带有“ stack”。
内森·戴维斯

这里唯一的一点是,某些语言对使用调用堆栈有严格的限制。
Will Ness
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.