Algorithm 如何在树形数据结构上实现尾部递归

Algorithm 如何在树形数据结构上实现尾部递归,algorithm,recursion,scheme,racket,tail-recursion,Algorithm,Recursion,Scheme,Racket,Tail Recursion,我正在使用Racket,但这个问题将适用于任何支持尾部递归的方案 我熟悉在平面列表上实现尾部递归的传统模式,大致如下: (define (func x [acc null]) (if (null? x) acc (func (cdr x) (cons (do-something-to (car x)) acc)))) 在这种情况下,func处于尾部位置 但当我使用一棵树,即一个带有递归嵌套列表的列表时,我最终会使用映射进行递归下降,如下所示: 这可以工作,但func

我正在使用Racket,但这个问题将适用于任何支持尾部递归的方案

我熟悉在平面列表上实现尾部递归的传统模式,大致如下:

(define (func x [acc null])
  (if (null? x)
      acc
      (func (cdr x) (cons (do-something-to (car x)) acc))))
在这种情况下,func处于尾部位置

但当我使用一棵树,即一个带有递归嵌套列表的列表时,我最终会使用映射进行递归下降,如下所示:

这可以工作,但func2不再处于尾部位置

你能——如果是的话,你会如何——以尾部递归的方式重写func2


撇开它是否能提高性能这个问题不谈,这不是我要问的问题。

通过引入一个充当堆栈的累加器,从技术上讲,您可以做到这一点。只有当堆栈为空时,才能执行函数


但是,这与使用函数调用堆栈(即非尾部递归)具有相同的内存使用要求,因此通常这样做不会带来任何好处。

通过引入充当堆栈的累加器,技术上可以做到这一点。只有当堆栈为空时,才能执行函数


但是,这与使用函数调用堆栈(即非尾部递归)具有相同的内存使用要求,因此通常这样做不会带来任何好处。

正如在另一个答案中已经正确陈述和解释的那样,在这方面使用尾部递归没有任何好处。但是,由于您对它的实现方式感兴趣,这里是我曾经实现过的一个深度映射函数。如果您对镜像列表感到满意,那么代码会更干净

(define deep-map
  (λ (f lst)
    (let tail-rec ([stack `(,lst)] [acc '(())])
      ;(displayln (~a "Stack: " stack " / Acc: " acc))
      (cond [(null? (car stack))
             (if (null? (cdr stack))
                 (car acc)
                 (tail-rec (cdr stack)
                           `(,(append (cadr acc) `(,(car acc))) . ,(cddr acc))))]
            ;; The first element is a list and is being put on the stack
            [(list? (caar stack))
             (tail-rec `(,(caar stack) . (,(cdr (car stack)) . ,(cdr stack)))
                       `(() . ,acc))]
            ;; Process next element
            [else (tail-rec `(,(cdar stack) . ,(cdr stack)) 
                            `(,(append (car acc) `(,(f (caar stack)))) . ,(cdr acc)))]
            ))))
一个简单的例子:

> (deep-map add1 '(1 ((2) 3)))
Stack: ((1 ((2) 3))) / Acc: (())
Stack: ((((2) 3))) / Acc: ((2))
Stack: (((2) 3) ()) / Acc: (() (2))
Stack: ((2) (3) ()) / Acc: (() () (2))
Stack: (() (3) ()) / Acc: ((3) () (2))
Stack: ((3) ()) / Acc: (((3)) (2))
Stack: (() ()) / Acc: (((3) 4) (2))
Stack: (()) / Acc: ((2 ((3) 4)))
'(2 ((3) 4))

正如在另一个答案中已经正确陈述和解释的那样,在这个问题上使用尾部递归没有任何好处。但是,由于您对它的实现方式感兴趣,这里是我曾经实现过的一个深度映射函数。如果您对镜像列表感到满意,那么代码会更干净

(define deep-map
  (λ (f lst)
    (let tail-rec ([stack `(,lst)] [acc '(())])
      ;(displayln (~a "Stack: " stack " / Acc: " acc))
      (cond [(null? (car stack))
             (if (null? (cdr stack))
                 (car acc)
                 (tail-rec (cdr stack)
                           `(,(append (cadr acc) `(,(car acc))) . ,(cddr acc))))]
            ;; The first element is a list and is being put on the stack
            [(list? (caar stack))
             (tail-rec `(,(caar stack) . (,(cdr (car stack)) . ,(cdr stack)))
                       `(() . ,acc))]
            ;; Process next element
            [else (tail-rec `(,(cdar stack) . ,(cdr stack)) 
                            `(,(append (car acc) `(,(f (caar stack)))) . ,(cdr acc)))]
            ))))
一个简单的例子:

> (deep-map add1 '(1 ((2) 3)))
Stack: ((1 ((2) 3))) / Acc: (())
Stack: ((((2) 3))) / Acc: ((2))
Stack: (((2) 3) ()) / Acc: (() (2))
Stack: ((2) (3) ()) / Acc: (() () (2))
Stack: (() (3) ()) / Acc: ((3) () (2))
Stack: ((3) ()) / Acc: (((3)) (2))
Stack: (() ()) / Acc: (((3) 4) (2))
Stack: (()) / Acc: ((2 ((3) 4)))
'(2 ((3) 4))

关于性能,DrRacket中的一些快速测试表明,deep map大约比简单的递归下降循环慢一个数量级,DrRacket中的一些快速测试表明,deepmap比简单的递归下降循环慢大约一个数量级。除了累加器可以是无限的,而堆栈不能。这取决于实现。例如,如果您通过使用CPS转换而不是堆栈复制来实现continuations,那么您的调用堆栈实际上将驻留在堆中,并且不受CPU堆栈的限制。@ChristopherDetroyer:即使没有Chris提到的CPS hijinks,实际上也没有理由假设堆比堆栈大。至少在理论上,您可以使用链接器开关为堆栈保留2^字大小的VM字节,操作系统会根据需要将其分页。实际限制可能稍低,并且取决于操作系统。除了累加器可以是无限的,而堆栈不能。这取决于实现。例如,如果您通过使用CPS转换而不是堆栈复制来实现continuations,那么您的调用堆栈实际上将驻留在堆中,并且不受CPU堆栈的限制。@ChristopherDetroyer:即使没有Chris提到的CPS hijinks,实际上也没有理由假设堆比堆栈大。至少在理论上,您可以使用链接器开关为堆栈保留2^字大小的VM字节,操作系统会根据需要将其分页。实际限制可能稍低,并且取决于操作系统。继续传递样式是另一种选择。我现在没有时间发布完整的答案,但是展示了一种借助于CPS的尾部递归方法来遍历和替换树。延续传递样式是另一种选择。我现在没有时间发布完整的答案,但是展示了一种借助CPS在树中遍历和替换的尾部递归方法。