为什么Haskell List的`++`递归实现并花费O(n)时间?

时间:2015-06-07 10:50:31

标签: list pointers haskell recursion functional-programming

据我所知,Haskell中的List类似于C语言中的Linked-List。

所以对于下面的表达式:

a = [1,2,3]
b = [4,5,6]
a ++ b

Haskell以递归的方式实现这个:

(++) (x:xs) ys = x:xs ++ ys

时间复杂度为O(n) ..

但是,我想知道为什么我不能更有效地实施++

最有效的方式可能是这样的:

  1. 制作a的副本(分叉),我们称之为a',在O(1)时间内可能会有一些技巧

  2. 使a'的最后一个元素指向b的第一个元素。这可以在O(1)时间内轻松完成..

  3. 有没有人有这方面的想法?谢谢!

3 个答案:

答案 0 :(得分:17)

这几乎就是递归解决方案的作用。复制a需要O(n)(其中na的长度。b的长度不会影响复杂性。)

在O(1)时间内复制n元素列表确实没有“技巧”。

答案 1 :(得分:12)

请参阅 copy(fork)部分是问题 - 递归解决方案就是这样做的(你真的必须这样做,因为你必须调整{中元素的所有指针) {1}}列表。

我们说aa = [a1,a2,a3]是一些列表。

您必须制作b的新副本(我们称之为a3),因为它现在不再指向空列表,而是指向a3'的开头。

然后你必须复制第二个到最后一个元素b,因为它必须指向a2,最后 - 出于同样的原因 - 你必须创建一个新的{{1}副本1}}(指向a3')。

这正是递归定义所做的 - 它对算法来说没有问题 - 它是数据结构的一个问题(它只是对连接不好)。

如果你不允许可变性并且想要列表的结构,你真的可以做其他事情。

你在其他langs中有这个。如果它们提供不可变数据 - 例如.net字符串是不可变的 - 所以字符串连接几乎与此处相同(如果你连接很多字符串,你的程序将表现不佳)。有一种解决方法(a1)可以更好地处理内存占用 - 但当然这些不再是不可变的数据结构。

答案 2 :(得分:1)

没有办法在恒定时间内进行连接,只是因为数据结构的不变性不允许它。

您可能认为可以执行类似于“cons”运算符(:)的操作,该操作符会向列表{{前面>添加额外的元素x0 1}}(导致oldList=[x1,x2,x3])而不必遍历整个列表。但这只是因为您没有触及现有列表newList=(x0:oldLIst),而只是引用它。

oldList

但是在你的情况下(x0 : ( x1 : ( x2 : ( x3 : [] ) ) ) ^ ^ newList oldList )我们正在谈论在数据结构中深入更新引用。您想要用新尾a ++ b替换[]中的1:(2:(3:[]))[1,2,3]的显式形式)。算一下括号,你就会发现我们必须深入到b。这总是很昂贵,因为我们必须复制整个外部部分,以确保[]保持不变。在结果列表中,旧的a指向哪里以获得未修改的列表?

a

在同一数据结构中这是不可能的。所以我们需要第二个:

1  :  ( 2  :  ( 3  :  b  )   )
^                     ^
a++b                  b

这意味着要复制那些1 : ( 2 : ( 3 : [] ) ) ^ a 个节点,这些节点必须花费第一个列表中提到的线性时间。因此,您提到的“复制(分叉)”与您所说的不同,在O(1)中不是

  

制作a的副本(fork),我们称之为',在O(1)时间内可能会有一些技巧

当你谈到一个“技巧”来在不断的时间内分叉时,你可能会考虑不实际制作完整副本,而是创建对原始:的引用,并将更改存储为“注释” (如提示:“修改尾部:使用a代替b”。

但是这就是Haskell,无论如何,由于它的懒惰,它仍然存在!它不会立即执行O(n)算法,而只是“记住”您想要一个连接列表,直到您实际访问其元素。但这并不能帮助您最终支付费用。因为即使在开始时引用很便宜(在O(1)中,就像你想要的那样),当你访问实际的列表元素时,[]运算符的每个实例都会增加一点开销(成本) “将您添加到引用中的注释”解释为对连接的第一部分中的每个元素的访问,最终有效地添加O(n)成本。