【问题标题】:tail recursion vs. forward recursion尾递归与前向递归
【发布时间】:2011-03-03 20:17:53
【问题描述】:

谁能告诉我这两种递归和示例之间的区别(特别是在 OCaml 中)?

【问题讨论】:

    标签: recursion functional-programming ocaml


    【解决方案1】:

    例如,递归函数 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 接受的答案很好地解释了前向递归。

    【讨论】:

      【解决方案2】:

      尾递归函数是一个函数,其中唯一的递归调用是函数中的最后一个。非尾递归函数是不是这种情况的函数。

      向后递归是在每次递归调用中参数的值小于上一步的递归。前向递归是一种递归,它每一步都会变大。

      这是两个正交的概念,即前向递归可能是尾递归,也可能不是尾递归,这同样适用于后向递归。

      例如,阶乘函数在命令式语言中通常是这样编写的:

      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)。

      【讨论】:

        【解决方案3】:

        这是一个尾递归阶乘函数的示例:

        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 执行尾调用优化,从而导致堆栈不溢出。通常,尾递归函数会利用累加器值来进行尾调用优化。

        【讨论】:

        • 除非我误解了前向递归的含义(我承认我必须用谷歌搜索,因为我以前从未听说过这个术语),否则你的函数都不是前向递归的。
        • 看起来我错过了问题的“前向”递归部分。
        猜你喜欢
        • 1970-01-01
        • 2019-10-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-03-21
        • 2017-11-11
        • 2018-03-17
        相关资源
        最近更新 更多