这样的函数结构是尾递归吗?
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))))))
基于答案,我假设第一个不是纯尾递归。
答案 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
值,它也非常慢。通过使用附加参数a
和b
表示我们的计算状态,可以实现 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)
我认为这里棘手的是,有两种不同的方式来考虑尾递归函数。
首先,有纯粹的尾部递归函数,这些函数的唯一递归是使用尾部递归完成的。对于上面的情况,您的功能不是纯粹的尾递归,因为递归分支,而纯尾递归不能分支。
第二,有些函数可以通过使用尾部调用优化来消除某些递归。这些函数可以执行他们想执行的任何类型的递归,但是具有至少一个递归调用,可以使用尾部调用优化以非递归方式对其进行重写。您拥有的函数确实属于此类,因为编译器可以
foo(data, x)
,但是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