据我所知,Haskell中的List类似于C语言中的Linked-List。
所以对于下面的表达式:
a = [1,2,3]
b = [4,5,6]
a ++ b
Haskell以递归的方式实现这个:
(++) (x:xs) ys = x:xs ++ ys
时间复杂度为O(n)
..
但是,我想知道为什么我不能更有效地实施++
。
最有效的方式可能是这样的:
制作a
的副本(分叉),我们称之为a'
,在O(1)
时间内可能会有一些技巧
使a'
的最后一个元素指向b
的第一个元素。这可以在O(1)
时间内轻松完成..
有没有人有这方面的想法?谢谢!
答案 0 :(得分:17)
这几乎就是递归解决方案的作用。复制a
需要O(n)(其中n
是a
的长度。b
的长度不会影响复杂性。)
在O(1)时间内复制n
元素列表确实没有“技巧”。
答案 1 :(得分:12)
请参阅 copy(fork)部分是问题 - 递归解决方案就是这样做的(你真的必须这样做,因为你必须调整{中元素的所有指针) {1}}列表。
我们说a
和a = [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)成本。