这样的功能结构尾部递归吗?

时间:2018-12-28 20:20:53

标签: recursion scheme pseudocode tail-recursion

这样的函数结构是尾递归吗?

function foo(data, acc) {
    ...
    return foo(data, foo(data, x));
}

根据定义,当递归调用是该函数最后执行的操作时,递归函数是尾递归。在此示例中,该函数所做的最后一件事是调用foo并返回其值,但是在此之前,它使用了嵌套foo函数的返回值。因此我很困惑。

编辑: 考虑方案语言和一个简单函数,该函数将给定列表中的元素相乘:

示例1:

(define (foo list) (helper list 1) )

(define (helper list acc)
  (cond ((null? list) acc)
   ((not (pair? list)) (* list acc))
   ((list? (car list)) (helper (car list) (helper (cdr list) acc)))
   (else (helper (cdr list) (* acc (car list)))))
)

示例2:这是一种纯尾递归吗?

(define (foo list) (helper list 1) )

(define (helper list acc)
    (cond ((null? list) acc)
    ((not (pair? list)) (* list acc))
    ((list? (car list)) (helper (cdr list) (* (foo (car list)) acc)))
    (else (helper (cdr list) (* acc (car list))))))

基于答案,我假设第一个不是纯尾递归。

4 个答案:

答案 0 :(得分:3)

否,它不是尾部递归的,因为foo被从尾部位置调出-

function foo(data, acc) {
    ...
                     // foo not in tail position here
    return foo(data, foo(data, x));
}

让我们使用fibonacci-

这样的具体程序来完成此工作

const fibonacci = n =>
  n < 2
    ? n                   // non tail call!
    : fibonacci (n - 2) + fibonacci (n - 1)
    
console .log (fibonacci (10)) // 55

上面,递归fibonacci可以对fibonacci产生两个调用,每个调用都可以对fibonacci产生两个 more 调用。如果不重写它,则两个调用都不可能处于尾部位置。我们可以使用一个辅助函数来解决该问题,该函数具有一个附加参数,位于下面的then-

const helper = (n, then) =>
{ if (n < 2)
    return then (n)                // tail
  else 
    return helper (n - 2, a =>     // tail
             helper (n - 1, b =>   // tail
               then (a + b)        // tail
           ))
}

const fibonacci = n =>
{ return helper (n, x => x)        // tail
}
    
console .log (fibonacci (10)) // 55

某些语言允许您指定默认参数,因此无需使用单独的辅助功能-

const identity = x =>
  x

const fibonacci = (n, then = identity) =>
{ if (n < 2)
    return then (n)                  // tail
  else 
    return fibonacci (n - 2, a =>    // tail
             fibonacci (n - 1, b =>  // tail
               then (a + b)          // tail
           ))
}

console .log (fibonacci (10))
// 55

fibonacci (10, res => console .log ("result is", res))
// result is: 55

是否尾部递归,上面的fibonacci是一个指数过程,即使是很小的n值,它也非常慢。通过使用附加参数ab表示我们的计算状态,可以实现 linear 处理-

const fibonacci = (n, a = 0, b = 1) =>
  n === 0
    ? a                            // tail
    : fibonacci (n - 1, b, a + b)  // tail
  
console .log
  ( fibonacci (10)   // 55
  , fibonacci (20)   // 6765
  , fibonacci (100)  // 354224848179262000000
  )

有时您需要使用其他状态参数,有时您需要使用辅助函数或诸如then之类的延续。

如果您使用特定语言给我们提供特定问题,我们可能会写出更具体的答案。


在已编辑的问题中,您包括一个Scheme程序,该程序可以将嵌套的数字列表相乘。我们首先展示then技术

(define (deep-mult xs (then identity))
  (cond ((null? xs)
         (then 1))
        ((list? (car xs))
         (deep-mult (car xs) ;; tail
                    (λ (a)
                      (deep-mult (cdr xs) ;; tail
                                 (λ (b)
                                   (then (* a b)))))))
        (else
         (deep-mult (cdr xs) ;; tail
                    (λ (a)
                      (then (* a (car xs))))))))

(deep-mult '((2) (3 (4) 5))) ;; 120

您也可以像在第二种方法中一样使用状态参数acc,但是由于输入可以嵌套,因此必须使用then技术来平整潜在的 two < / em>调用deep-mult-

(define (deep-mult xs (acc 1) (then identity))
  (cond ((null? xs)
         (then acc)) ;; tail
        ((list? (car xs))
         (deep-mult (car xs) ;; tail
                    acc
                    (λ (result)
                      (deep-mult (cdr xs) result then)))) ;; tail
        (else
         (deep-mult (cdr xs) ;; tail
                    acc
                    (λ (result) then (* result (car xs)))))))

(deep-mult '((2) (3 (4) 5)))
;; 120

我不太喜欢这个版本的程序,因为每种技术只能解决一半的问题,而以前只使用一种 技术。

对于此特定问题,也许聪明的解决方法是在嵌套列表的情况下使用append

(define (deep-mult xs (acc 1))
  (cond ((null? xs)
         acc)
        ((list? (car xs))
         (deep-mult (append (car xs) ;; tail
                            (cdr xs))
                    acc))
        (else
         (deep-mult (cdr xs) ;; tail
                    (* acc (car xs))))))

(deep-mult '((2) (3 (4) 5))) 
;; 120

但是,append是一个昂贵的列表操作,对于非常深层嵌套的列表,此过程可能会导致性能下降。当然,还有其他解决方案。看看您能提出什么,并提出其他问题。之后,我将分享一个我认为提供最多优点和最少缺点的解决方案。

答案 1 :(得分:2)

我认为这里棘手的是,有两种不同的方式来考虑尾递归函数。

首先,有纯粹的尾部递归函数,这些函数的唯一递归是使用尾部递归完成的。对于上面的情况,您的功能不是纯粹的尾递归,因为递归分支,而纯尾递归不能分支。

第二,有些函数可以通过使用尾部调用优化来消除某些递归。这些函数可以执行他们想执行的任何类型的递归,但是具有至少一个递归调用,可以使用尾部调用优化以非递归方式对其进行重写。您拥有的函数确实属于此类,因为编译器可以

  1. 具有使用真正的递归进行的递归调用foo(data, x),但是
  2. 回收堆栈空间以评估foo(data, /* result of that call */)

那么您的函数纯粹是尾递归吗?不,因为递归分支。但是一个好的编译器可以优化掉那些递归调用之一吗?是的。

答案 2 :(得分:0)

通常,通过命名所有临时实体将函数转换为SSA form,然后查看对您的foo的每次调用是否处于尾部位置,即最后要做的事情。

您问,怎么会有一个以上的尾巴位置?他们可以将每个条件放在自己的条件分支中,而条件本身就位于尾部位置。

关于已编辑的lisp函数。两者都不是尾部递归的,甚至不是helper在非尾位置调用foo的最后一个,因为foo最终也会调用helper。是的,要完全尾部递归,必须确保没有在非尾部位置的调用会导致调用该函数本身。但是如果它处于尾部位置就可以了。叫声是美化的 goto ,这是这里的目标。

但是,您可以通过遍历输入嵌套列表nay tree 数据结构来像这样

那样,以递归方式对此尾部进行编码
(define (foo list) (helper list 1) )

(define (helper list acc)
  (cond 
    ((null? list)  acc)
    ((not (pair? list))  (* list acc))
    ((null? (car list))  (helper (cdr list) acc))
    ((not (pair? (car list)))  (helper (cdr list) (* (car list) acc)))
    (else  (helper (cons (caar list)
                     (cons (cdar list) (cdr list))) acc))))

答案 3 :(得分:0)

这可能是挑剔的,但是 function 并不是说是尾递归的。 如果过程调用可以在尾部上下文中发生,则该调用被称为尾部调用。

在您的示例中:

setlocal /?

function foo(data, acc) {
    ...
    return foo(data, foo(data, x));
}

有两个调用:内部的(define (foo data acc) (foo data (foo data x))) 不在尾部上下文中, 但是外面的(foo data x)是。

有关R5RS方案中尾部上下文的规范,请参见[1]。

总结:检查特定呼叫是否为尾部呼叫是语法检查。

您的函数是“尾递归”吗?这取决于您如何定义“尾部递归函数”。如果您的意思是“所有递归调用都必须是尾调用”-否。 如果您的意思是“所有身体评估都以递归调用结尾”,那么可以。

现在同样重要的是函数的运行时行为。当您评估函数调用时,将发生什么样的计算过程?解释有点儿涉及,所以我将点一下,只引用一下:[2] SICP“ 1.2程序及其生成的过程。”

[1] http://www.dave-reed.com/Scheme/r5rs_22.html [2] https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-11.html#%_sec_1.2