ocaml 尾递归函数

问题描述 投票:0回答:2

我正在使用 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?).

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

ocaml stack-overflow tail-recursion
2个回答
3
投票

正如 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

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

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

更新 2

这是一个更高效的尾递归版本,它不需要 List.rev 和 String.length 调用:

let string_to_list str =
    let rec loop accum i =
        if i < 0 then
            accum
        else
            loop (str.[i] :: accum) (i - 1)
    in
    loop [] (String.length str - 1)

(事实上这是一个非常好的获取字符列表的方法,但是它没有教最重要的向后累加结果并在最后反转的技术。:-)

© www.soinside.com 2019 - 2024. All rights reserved.