【问题标题】:Tail recursive List.map尾递归 List.map
【发布时间】:2014-12-10 14:27:25
【问题描述】:

OCaml 中典型的 List.map 函数非常简单,它接受一个函数和一个列表,并将该函数递归地应用于列表的每个项目。我现在需要将 List.map 转换为尾递归函数,怎么做?累加器应该累加什么?

【问题讨论】:

  • 这听起来像是家庭作业(对我来说)。所以也许你应该首先建议认为它可以如何工作。
  • 这实际上不是作业,而是我正在学习的一些旧考试。但是我确实有一些想法,例如模式匹配列表,如果它为空则返回累加器(尾递归的标准做法),如果它的形式为 hd::tl 然后将 tl 传递给下一个函数并附加( f hd) 到累加器
  • 这对我来说听起来很棒。如果你这样做,你如何处理基本情况(当你的输入为空时)。那个时候的蓄能器是什么样子的?

标签: list ocaml tail-recursion


【解决方案1】:

可以说最简单的方法是使用尾递归辅助函数map_aux 来实现map,该函数遍历列表同时累积已映射的前缀:

let map f l =
  let rec map_aux acc = function
    | [] -> acc
    | x :: xs -> map_aux (acc @ [f x]) xs
  in
  map_aux [] l

但是,由于列表连接运算符@ 的时间与其第一个参数的长度呈线性关系,因此会产生二次时间遍历。此外,列表连接本身不是尾递归的。

因此,我们要避免使用@。此解决方案不使用列表连接,而是通过新映射的参数添加到累加器来累加:

let map f l =
  let rec map_aux acc = function
    | [] -> List.rev acc
    | x :: xs -> map_aux (f x :: acc) xs
  in
  map_aux [] l

为了以正确的顺序恢复映射的元素,我们只需在空列表的情况下反转累加器即可。请注意,List.rev 是尾递归的。

最后,这种方法通过累积所谓的difference list 来避免递归列表连接和列表反转:

let map f l =
  let rec map_aux acc = function
    | [] -> acc []
    | x :: xs -> map_aux (fun ys -> acc (f x :: ys)) xs
  in
  map_aux (fun ys -> ys) l

这个想法是让累积的前缀列表由一个函数 acc 表示,当应用于参数列表ys 时,返回前缀列表添加到ys。因此,作为累加器的初始值,我们有fun ys -> ys,表示一个空前缀,在[] 的情况下,我们只需将acc 应用于[] 以获得映射前缀。

【讨论】:

  • 差异列表法的内存使用情况如何?传递那些大的闭包闭包听起来比一个列表更麻烦......
  • @unhammer:据我所见,差异列表方法通常不如列表反转方法有效。确实:由于过多的闭包创建。
  • 那不是“不同列表”的延续传递风格吗?
  • 两年后:是的,@JonHarrop,是的。
【解决方案2】:

(我相信这不是家庭作业。)

答案是函数式编程中的经典模式之一,即 cons/reverse 习惯用法。首先,您以相反的顺序排列您的列表,这很容易以尾递归方式完成。最后,您反转列表。逆向也是尾递归操作,因此不会对堆栈安全造成问题。

缺点是额外的分配和更笨拙的代码。

let map f list =
  let rec loop acc = function
    | [] -> List.rev acc
    | x::xs -> loop (f x::acc) xs in
  loop [] list

一个好的练习是(重新)实现一堆“标准”列表函数(appendrev_appendfold_leftfold_rightfilterforall 等),使用这种风格使它们在必要时尾递归。

【讨论】:

  • 我觉得这个问题非常有趣,而且答案很有价值和有启发性。因此,无论任何外部环境如何,它本身都是有价值的。或者,换句话说,谁在乎是不是作业?!
猜你喜欢
  • 1970-01-01
  • 2016-03-21
  • 2018-03-17
  • 1970-01-01
  • 2017-11-11
  • 1970-01-01
  • 1970-01-01
  • 2019-10-30
  • 2011-03-03
相关资源
最近更新 更多