【问题标题】:How can I implement a tail-recursive list append?如何实现尾递归列表追加?
【发布时间】:2010-05-19 16:33:27
【问题描述】:

一个像这样的简单附加函数(在 F# 中):

let rec app s t =
   match s with
      | [] -> t
      | (x::ss) -> x :: (app ss t)

当 s 变大时会崩溃,因为函数不是尾递归的。我注意到 F# 的标准追加函数不会因大列表而崩溃,因此必须以不同的方式实现它。所以我想知道:追加的尾递归定义是什么样的?我想出了这样的事情:

let rec comb s t =
   match s with
      | [] -> t
      | (x::ss) -> comb ss (x::t)
let app2 s t = comb (List.rev s) t 

这可行,但看起来很奇怪。还有更优雅的定义吗?

【问题讨论】:

    标签: f# append tail-recursion


    【解决方案1】:

    传统(非尾递归)

    let rec append a b =
        match a, b with
        | [], ys -> ys
        | x::xs, ys -> x::append xs ys
    

    带有累加器(尾递归)

    let append2 a b =
        let rec loop acc = function
            | [] -> acc
            | x::xs -> loop (x::acc) xs
        loop b (List.rev a)
    

    带有延续(尾递归)

    let append3 a b =
        let rec append = function
            | cont, [], ys -> cont ys
            | cont, x::xs, ys -> append ((fun acc -> cont (x::acc)), xs, ys)
        append(id, a, b)
    

    将任何非尾递归函数转换为带有延续的递归函数非常简单,但我个人更喜欢累加器以提高可读性。

    【讨论】:

    • 在第一个示例中,如果 b 在所有模式中都相同,那么在 b 上进行模式匹配有什么意义?你可以简单地使用 b
    • 您确定它有效吗?我得到 > append2 [1;2] [3;4];;验证它:int list = [2; 3; 4] 和 > append3 [1;2] [3;4];;验证它:int list = [1; 3; 4] 虽然我没有看到错误,但 append2 对我来说看起来不错..
    • 很奇怪。您的代码非常好,它使用 fsc 运行。但不在 fsi 内运行。不是我在单声道上遇到 fsi 的第一个问题。
    • 最后一个观察:append2 和append3 之间的区别不仅仅是可读性:append2 [1..10000000] [] 有效,append3 [1..10000000] [] 导致堆栈溢出。
    • @martingw: append2 和 append3 是正确的,没有任何理由你会得到错误的结果。此外,append3 是尾递归的,但如果您在调试模式下运行代码,您仍然可能会出现堆栈溢出,因为默认情况下禁用尾调用(请参阅stackoverflow.com/questions/1416415/…)。
    【解决方案2】:

    除了朱丽叶发布的内容:

    使用序列表达式
    在内部,序列表达式会生成尾递归代码,因此可以正常工作。

    let append xs ys = 
      [ yield! xs
        yield! ys ]
    

    使用可变的 .NET 类型
    David 提到 F# 列表可以改变 - 但是仅限于 F# 核心库(并且该功能不能被用户使用,因为它破坏了功能概念)。您可以使用可变的 .NET 数据类型来实现基于突变的版本:

    let append (xs:'a[]) (ys:'a[]) = 
      let ra = new ResizeArray<_>(xs)
      for y in ys do ra.Add(y)
      ra |> List.ofSeq
    

    这在某些情况下可能很有用,但我通常会避免 F# 代码中的突变。

    【讨论】:

      【解决方案3】:

      快速浏览一下 F# 源代码,尾巴似乎在内部是可变的。一个简单的解决方案是在将第一个列表的元素放入第二个列表之前反转第一个列表。这与反转列表一起,递归地实现 tail 是微不足道的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-12-15
        • 2019-04-07
        • 2012-04-10
        • 2020-05-05
        • 1970-01-01
        • 2019-11-05
        • 2023-03-21
        • 2021-02-06
        相关资源
        最近更新 更多