繁体   English   中英

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

[英]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) ..

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

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

  1. 制作的副本(叉) a ,我们称之为a' ,可能有一些技巧,以做到这一点O(1)时间

  2. 使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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM