如何在函数程序中实现尾递归

时间:2015-03-11 10:17:53

标签: recursion functional-programming scheme lisp common-lisp

例如,在Scheme中使用以下简单的右侧折叠实现:

(define (fold-rite kons knil clist)
  (if (null? clist)
    knil
    (kons (car clist) (fold-rite kons knil (cdr clist)))))

这显然不是尾部调用消除的候选者,因为fold-rite的递归调用必须在调用kons之前完成。现在,我可能会有点聪明,而是使用延续传递方式:

(define (fold-rite-cps kons knil clist kontinuation)
  (if (null? clist)
    (kontinuation knil)
    (fold-rite-cps kons knil (cdr clist) 
      (lambda (x) (kontinuation (kons (car clist) x))))))

现在fold-rite-cps是尾递归的,但在递归期间建立的中间连续仍然必须保留在某处。因此,虽然我可能没有吹出堆栈,但我仍然使用与第一个版本一样多的内存,并且不知何故我感觉首先收集了大量的延续,然后一举将它们全部解开应该是性能不好,虽然第二个版本在我的机器上实际上要快得多。

但是如果左侧折叠可以在恒定空间中运行(减去累积的值)并且列表上的右侧折叠与其反向上的左侧折叠相同,那么我想应该有一种方法来实现尾部 - 也可以在恒定空间中运行的递归右折叠。如果是这样,我该怎么做?更一般地说,有什么方法可以将非尾递归函数转换成尾递归函数,最好是可以在常量空间中运行的函数(假设显式循环可以用也在常量运行的命令式语言编写)空间)?我做了什么错误的假设吗?

虽然我已经标记了Scheme和Lisp,但我对任何语言的解决方案感兴趣;这些方法应该适用于一般的功能程序。

1 个答案:

答案 0 :(得分:1)

基于上面的评论,我认为在不改变数据结构(缺点单元格)的情况下,最好的答案将如下(在常见的lisp中,因为这是我的方便)。

由于cons单元的单个链接结构,为了不积累空间,我们将不得不几乎切换列表的顺序,然后折叠反转列表。反转是线性空间操作,根据所使用的减少函数,减少可以为常数空间。

(defun foldl (op clist &optional base)
  (if (null clist)
      base
      (foldl op (cdr clist)
             (funcall op (car clist) base))))

(defun foldr (op clist &optional base)
  ;; reverse the list then fold it
  (foldl op (foldl #'cons clist nil) base))

直接回答:不,不可能在恒定空间中折叠,因为cons单元的单个链接性质需要完整列表遍历才能到达最后一个元素,然后是展开步骤或单独的列表来跟踪实际折叠操作。