【问题标题】:ocaml tail-recursive functionsocaml 尾递归函数
【发布时间】:2021-10-11 16:01:54
【问题描述】:

当我在很长的输入上运行以下函数时,我正在使用 utop 运行 Ocaml:

let string_to_list str =
  let rec loop i limit =
    if i = limit then []
    else (String.get str i) :: (loop (i + 1) limit)
  in
  loop 0 (String.length str);;

它返回以下错误:

Stack overflow during evaluation (looping recursion?).

函数的尾递归版本是什么?

【问题讨论】:

  • 当然,它不会是一个stack overflow 超过输入较小长度的字符串。你尝试了什么样的输入,它溢出了什么样的输入?这件事有时会帮助您理解......为什么需要tail-recursive 解决方案而不是recursive 解决方案,这种情况几乎适用于我们将使用的每种功能语言。否则,recursive 样式是指定解决方案的最清晰、简洁和优雅的方式。

标签: ocaml stack-overflow tail-recursion


【解决方案1】:

正如 Jeffrey Scofield 所说,您的函数不是尾递归的。将其转换为使用尾递归将涉及向您的 loop 函数引入一个累加器参数。

let string_to_list str =
  let rec loop i limit acc =
    if i = limit then acc
    else 
      let ch = String.get str i in
      loop (i + 1) limit (ch :: acc)
  in
  loop 0 (String.length str) [] |> List.rev

这样,评估函数所需的所有信息都包含在一个堆栈帧中,编译器可以优化掉所有先前的堆栈帧。

为了在纯文本中提供一点视觉效果,我们查看函数的非尾递归版本,在 "hello" 上调用。

+-----------------------+
| string_to_list "hello"|->
+-----------------------+ |
^      v------------------+
| +--------+
<-|loop 0 5|->
  +--------+ |
  ^      v---+
  | +--------+
  <-|loop 1 5|->
    +--------+ |
    ^      v---+
    | +--------+
    <-|loop 2 5|->
      +--------+ |
      ^      v---+
      | +--------+
      <-|loop 3 5|->
        +--------+ |
        ^      v---+
        | +--------+
        <-|loop 4 5|->
          +--------+ |
          ^      v---+
          | +--------+
          <-|loop 5 5|
            +--------+

在我们进行过程中,每次通话都需要前一次通话中的信息。但是,如果 loop 通过参数传递所有必要的信息(即累积的结果列表),则 loop 的迭代不需要任何先前的调用来完全评估。

     +-----------------------+
     | string_to_list "hello"|
     +-----------------------+
                |
                v
+----------------------------------+
|loop 0 5 []                       |
+----------------------------------+
                |
                v
+----------------------------------+
|loop 1 5 ['h']                    |
+----------------------------------+
                |
                v
+----------------------------------+
|loop 2 5 ['e'; 'h']               |
+----------------------------------+
                |
                v
+----------------------------------+
|loop 3 5 ['l'; 'e'; 'h'].         |
+----------------------------------+
                |
                v
+----------------------------------+
|loop 4 5 ['l'; 'l'; 'e'; 'h']     |
+----------------------------------+
                |
                v
+----------------------------------+
|loop 5 5 ['o'; 'l'; 'l'; 'e'; 'h']|
+----------------------------------+
                |
                v
+----------------------------------+
|List.rev ['o'; 'l'; 'l'; 'e'; 'h']|
+----------------------------------+
                |
                v
+----------------------------------+
|['h'; 'e'; 'l'; 'l'; 'o']         |
+----------------------------------+

【讨论】:

    【解决方案2】:

    loop 函数不是尾递归的。你可以看到它对递归调用的返回值应用了一个操作(::)。这可以防止它是尾递归的。即递归调用不在尾部。

    更新

    这是一个尾递归函数,用于将字符串更改为字符列表。我希望我没有为你做作业。

    let string_to_list str =
        let rec loop accum i =
            if i >= String.length str then
                List.rev accum
            else
                loop (String.get str i :: accum) (i + 1)
        in
        loop [] 0
    

    正如我在评论中所说,这是你绝对必须熟悉的东西才能进行函数式编程。但是,一旦你看到了窍门,这并不难。

    通常需要以相反的顺序累积列表,然后在最后将其反转。添加到列表的末尾太慢(它往往会产生二次复杂度)。

    【讨论】:

    • 感谢您的回答!我怎样才能让它尾递归?
    • 古老的技巧是将累积的答案作为参数传递给递归调用,而不是对返回值应用操作。如果你不知道这个方法,学习是很重要的。我会把它添加到我的答案中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-10-17
    • 1970-01-01
    • 2013-05-20
    • 2020-04-07
    • 2013-11-15
    • 2019-09-14
    • 1970-01-01
    相关资源
    最近更新 更多