[英]Why is `++` for Haskell List implemented recursively and costs O(n) time?
据我了解,Haskell中的列表类似于C语言中的链接列表。
因此,对于以下表达式:
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)
时间轻松完成。
有人对此有想法吗? 谢谢!
这几乎就是递归解决方案所要做的。 它的复制a
这需要O(n)(其中n
是长度a
。的长度b
不影响复杂性)。
确实没有“技巧”可以在O(1)时间中复制n
元素的列表。
看到copy(fork)部分是问题所在-递归解决方案恰好做到了这一点(您确实必须这样做,因为您必须调整a
列表中元素的所有指针。
假设a = [a1,a2,a3]
, b
是一些列表。
您必须制作a3
的新副本(我们将其称为a3'
),因为它现在不再指向空列表,而是指向b
的开头。
然后,您还必须复制倒数第二个元素a2
因为它必须指向a3'
,最后-由于相同的原因-您还必须创建一个新的a1
副本(指向a2'
)。
这正是递归定义所做的工作-算法没问题-数据结构有问题(并发连接不好)。
如果您不允许可变性并且想要列表的结构,那么您实际上什么也做不了。
您还有其他语言。 如果它们提供不可变的数据也是如此(例如,.net中的字符串是不可变的),那么字符串连接的问题几乎与此处相同(如果连接大量字符串,则程序性能会很差)。 有一些变通方法( StringBuilder
)可以更好地处理内存占用量-但当然,这些不再是不可变的数据结构。
仅由于数据结构的不可变性不允许这样做,所以无法在恒定时间内进行这种连接。
你可能会认为你可以做类似的“利弊”操作符的东西( :
),增加了一个额外的元素x0
到列表的前面 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
这意味着复制那些:
节点,这必然会花费第一列表中提到的线性时间。 因此,您提到的“ copy(fork)”与您所说的不同, 而不是在O(1)中。
制作a的副本(叉子),我们称它为a',在O(1)时间内可能有一些技巧
当您谈论一个“技巧”以恒定的时间进行分叉时,您可能会考虑实际上并不制作完整副本,而是创建对原始a
的引用,并将更改存储为“注释”(如提示:“修改”尾:使用b
代替[]
“)。
但是无论如何,Haskell正是这样做的,因为它的懒惰! 它不会立即执行O(n)算法,而只是“记住”您想要一个串联列表,直到您实际访问其元素为止。 但这并不能避免您最终支付费用。 因为即使开始时引用很便宜(就像您想要的那样,在O(1)中也是如此),所以当您访问实际的列表元素时, ++
运算符的每个实例都会增加一点开销(“解释注释”(您添加到引用中的注释))来访问串联第一部分中的每个元素,最终有效地增加了O(n)成本。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.