【发布时间】:2011-03-03 20:17:53
【问题描述】:
谁能告诉我这两种递归和示例之间的区别(特别是在 OCaml 中)?
【问题讨论】:
标签: recursion functional-programming ocaml
谁能告诉我这两种递归和示例之间的区别(特别是在 OCaml 中)?
【问题讨论】:
标签: recursion functional-programming ocaml
例如,递归函数 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(这个链接是递归的)。
尾调用版本如下所示:
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 接受的答案很好地解释了前向递归。
【讨论】:
尾递归函数是一个函数,其中唯一的递归调用是函数中的最后一个。非尾递归函数是不是这种情况的函数。
向后递归是在每次递归调用中参数的值小于上一步的递归。前向递归是一种递归,它每一步都会变大。
这是两个正交的概念,即前向递归可能是尾递归,也可能不是尾递归,这同样适用于后向递归。
例如,阶乘函数在命令式语言中通常是这样编写的:
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)。
【讨论】:
这是一个尾递归阶乘函数的示例:
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 执行尾调用优化,从而导致堆栈不溢出。通常,尾递归函数会利用累加器值来进行尾调用优化。
【讨论】: