尾递归
使用常规递归,每个递归调用将另一个条目推送到调用堆栈。递归完成后,应用程序必须将每个条目一直弹回。如果有很多递归函数调用,它最终会有一个巨大的堆栈。
如果在尾部位置找到递归调用,Scala 会自动删除递归。可以将注释(@tailrec
)添加到递归函数以确保执行尾调用优化。如果编译器无法优化你的递归,则会显示错误消息。
定期递归
这个例子不是尾递归的,因为当进行递归调用时,函数需要跟踪调用返回后它需要对结果进行的乘法。
def fact(i : Int) : Int = {
if(i <= 1) i
else i * fact(i-1)
}
println(fact(5))
使用参数调用函数将产生如下所示的堆栈:
(fact 5)
(* 5 (fact 4))
(* 5 (* 4 (fact 3)))
(* 5 (* 4 (* 3 (fact 2))))
(* 5 (* 4 (* 3 (* 2 (fact 1)))))
(* 5 (* 4 (* 3 (* 2 (* 1 (fact 0))))))
(* 5 (* 4 (* 3 (* 2 (* 1 * 1)))))
(* 5 (* 4 (* 3 (* 2))))
(* 5 (* 4 (* 6)))
(* 5 (* 24))
120
如果我们尝试使用 @tailrec
注释此示例,我们将收到以下错误消息:could not optimize @tailrec annotated method fact: it contains a recursive call not in tail position
尾递归
在尾递归中,首先执行计算,然后执行递归调用,将当前步骤的结果传递给下一个递归步骤。
def fact_with_tailrec(i : Int) : Long = {
@tailrec
def fact_inside(i : Int, sum: Long) : Long = {
if(i <= 1) sum
else fact_inside(i-1,sum*i)
}
fact_inside(i,1)
}
println(fact_with_tailrec(5))
相反,尾递归阶乘的堆栈跟踪如下所示:
(fact_with_tailrec 5)
(fact_inside 5 1)
(fact_inside 4 5)
(fact_inside 3 20)
(fact_inside 2 60)
(fact_inside 1 120)
每次调用 fact_inside
时,只需要跟踪相同数量的数据,因为该函数只是将它所获得的值返回到顶部。这意味着即使调用 fact_with_tail 1000000
,它也只需要与 fact_with_tail 3
相同的空间量。非尾递归调用不是这种情况,因此大值可能导致堆栈溢出。