例如,一个函数通过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
提供了一个更具体的例子答案 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
慢一些,因为最后会反转列表的额外费用。但是,为了能够处理没有堆栈溢出的大型列表,这可能是一个可接受的代价。