尾递归与前向递归

时间:2010-06-15 06:22:56

标签: recursion functional-programming ocaml

有人可以告诉我这两种递归和示例之间的区别(特别是在OCaml中)吗?

3 个答案:

答案 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接受的答案中得到了很好的解释。