通过consing vs tail-call accumulator创建一个列表

时间:2013-03-24 13:05:19

标签: functional-programming sml

例如,一个函数通过consing创建一个列表:

fun example1 _ _ [] = []
  | example1 f g (x::xs) =
    if f x
    then (g x)::(example1 f g xs)
    else x::(example1 f g xs)

通过尾调用累加器创建一个列表:

fun example2 f g xs =
    let fun loop acc [] = acc
          | loop acc (x::xs') =
            if f x
            then loop (acc@[(g x)]) xs'
            else loop (acc@[x]) xs'
    in
        loop [] xs
    end

在给定相同参数的情况下生成相同的列表。

哪个功能有更好的运行时间?

追加操作@是否会遍历到列表的末尾以追加并最终获得与consing解决方案相同的运行时间,但使用的空间更少,代码更复杂一些?

consing或append是否创建了一个完整的新元素(对象的深层副本),即使原始元素没有变化,也只是重用现有元素?

这个问题为this question

提供了一个更具体的例子

1 个答案:

答案 0 :(得分:3)

x :: xs创建一个新的列表单元格,其头部为x,其尾部为xs。它不会创建xs的副本 - 既不深也不浅。所以这是O(1)操作。

xs @ [x]创建xs的浅表副本,其前一个节点的尾部现在为[x]。这是O(n)操作。

因此,example1函数的时间复杂度为O(n),而example2函数的时间复杂度为O(n^2)。两个函数都消耗O(n)个辅助空间。 example1因为它的堆栈使用而example2因为@在堆上创建了不属于结果列表的列表。

如果您更改example2以使用::而不是@,然后在到达列表末尾时对结果使用List.rev,则其运行时间将为O(n),但它仍然会比example1慢一些,因为最后会反转列表的额外费用。但是,为了能够处理没有堆栈溢出的大型列表,这可能是一个可接受的代价。