有人可以告诉我这两种递归和示例之间的区别(特别是在OCaml中)吗?
答案 0 :(得分:28)
尾递归函数是一个函数,其中唯一的递归调用是函数中的最后一个。非尾递归函数是一种不是这种情况的函数。
反向递归是一种递归,其中在每次递归调用中,参数的值小于上一步。正向递归是一种递归,其中每一步都会变大。
这是两个正交概念,即正向递归可能是尾递归也可能不是尾递归,同样适用于后向递归。
例如,阶乘函数通常用命令式语言编写:
fac = 1
for i from 1 to n:
fac := fac * i
阶乘的常见递归版本向后计数(即它以n-1
作为参数调用自身),但是如果你直接翻译上面的命令式解决方案,你会想出一个递归的版本向上。它看起来像这样:
let fac n =
let rec loop i =
if i >= n
then i
else i * loop (i+1)
in
loop 1
这是一个前向递归,你可以看到它比后向递归变量稍微麻烦,因为它需要一个辅助函数。现在这不是尾递归,因为loop
中的最后一次调用是乘法,而不是递归。所以为了使它具有尾递归性,你可以这样做:
let fac n =
let rec loop acc i =
if i >= n
then acc
else loop (i*acc) (i+1)
in
loop 1 1
现在这既是前向递归又是尾递归,因为递归调用是a)尾调用,b)调用自身的值越大(i+1
)。
答案 1 :(得分:8)
这是一个尾递归因子函数的例子:
let fac n =
let rec f n a =
match n with
0 -> a
| _ -> f (n-1) (n*a)
in
f n 1
这是非尾递归的对应物:
let rec non_tail_fac n =
match n with
0 -> 1
| _ -> (non_tail_fac n-1) * n
尾递归函数使用累加器a来存储前一次调用结果的值。这允许OCaml执行尾调用优化,这导致堆栈不溢出。通常,尾递归函数将使用累加器值来允许尾调用优化。
答案 2 :(得分:0)
例如,一个递归函数build_word
,它接受char list
并将它们组合成一个字符串,即['f'; 'o'; 'o']
到字符串"foo"
。诱导过程可以通过这种方式可视化:
build_word ['f'; 'o'; 'o']
"f" ^ (build_word ['o'; 'o'])
"f" ^ ("o" ^ (build_word ['o']) // base case! return "o" and fold back
"f" ^ ("o" ^ ("o"))
"f" ^ ("oo")
"foo"
这是正常的递归。请注意,每对括号代表一个新的堆栈帧或递归调用。这个问题的解决方案(即" f"," fo"或" foo")无法在递归结束之前导出(基本情况是满足)。只有这样,最后一帧才会将最后一个结果返回到前一个结果之前"弹出",反之亦然。
理论上,每次调用都会创建一个新的堆栈帧(或范围,如果你愿意的话)来保存"地点"从一开始就返回并收集碎片化的解决方案。这可能会导致stackoverflow(此链接是递归btw)。
尾部调用版本看起来像这样:
build_word ['f'; 'o'; 'o'] ""
build_word ['o'; 'o'], "f"
build_word ['o'] ("f" ^ "o")
build_word [] ("f" ^ "o" ^ "o")
"foo"
此处,累积结果(通常存储在称为accumulator
的变量中)正在向前传递。通过优化,尾调用不必创建新的堆栈帧,因为它不必维护先前的堆栈帧。解决方案正在被解决"转发"而不是"向后"。
以下是两个版本中的build_word
函数:
<强>非尾强>
let build_word chars =
match chars with
| [] -> None
| [c] -> Some Char.to_string c
| hd :: tl -> build_word tl
;;
<强>尾强>
let build_word ?(acc = "") chars =
match chars with
| [] -> None
| [c] -> Some Char.to_string c
| hd::tl -> build_word ~acc:(acc ^ Char.to_string hd) tl
;;
前向递归在@ sepp2k接受的答案中得到了很好的解释。