我正在学习haskell,并阅读了一些关于haskell列表和(插入您的语言)数组的性能差异的文章。
作为一个学习者,我显然只使用列表,而不考虑性能差异。我最近开始调查,发现哈斯克尔有许多可用的数据结构库。
有人能在不深入研究数据结构的计算机科学理论的情况下,解释列表、数组、向量和序列之间的区别吗?
此外,在使用一种数据结构而不是另一种数据结构时,是否有一些常见的模式?
是否有其他形式的数据结构是我所缺少的,并且可能有用?
- 看看这个关于列表和数组的答案:stackoverflow.com/questions/8196667/haskell-arrays-vs-lists向量与数组的性能基本相同,但API更大。
- 很高兴看到数据。地图也在这里讨论。这似乎是一个有用的数据结构,特别是对于多维数据。
摇滚乐列表
到目前为止,haskell中顺序数据最友好的数据结构是列表好的。
列表提供&1012;(1)缺点和模式匹配。标准库,也就是序曲,充满了有用的列表函数,这些函数应该丢弃您的代码(foldr、map、filter。列表是持久的,也就是纯功能的,非常好。haskell列表并不是真正的"列表",因为它们是共同产生的(其他语言称为这些流),所以好的。
1 2 3 4 5 6
| ones :: [Integer]
ones = 1:ones
twos = map (+1) ones
tenTwos = take 10 twos |
号
干得好。无限数据结构岩石。好的。
Haskell中的列表提供了一个类似于命令式语言中的迭代器的接口(因为懒惰)。所以,它们被广泛使用是有道理的。好的。另一方面
列表的第一个问题是索引到列表中需要花费时间,这很烦人。此外,附录可以是慢的++,但haskell的懒惰评估模型意味着,如果它们发生的话,它们可以被视为完全摊销。好的。
列表的第二个问题是它们的数据区域性较差。当内存中的对象不相邻排列时,真正的处理器会产生高常量。因此,在C++ EDCOX1中,5个词比我所知道的任何纯链表数据结构具有更快的"SNOC"(将对象放在末尾),尽管这不是一个持久性的数据结构,它比Haskell的列表更不友好。好的。
列表的第三个问题是它们的空间效率很差。一束额外的指针(通过一个常数因子)增加了你的存储空间。好的。序列是功能性的
Data.Sequence内部基于指状树(我知道,你不想知道这一点),这意味着它们有一些很好的特性。好的。
纯粹的功能。Data.Sequence是一种完全持久的数据结构。
快速访问树的开始和结束。ϴ;(1)(摊销)得到第一个或最后一个元素,或附加树。在事物列表中,最快的是,Data.Sequence最多是一个恒定的慢。
ϴ;(log n)访问序列中间。这包括插入值以生成新序列
高质量API
另一方面,Data.Sequence对数据局部性问题没有太大作用,只适用于有限的集合(它比列表要慢)好的。阵法不适合胆小的人。
数组是CS中最重要的数据结构之一,但它们不太适合懒惰的纯函数世界。数组提供&1012;(1)访问集合的中间部分和非常好的数据位置/常量因子。但是,因为它们不太适合哈斯克尔,所以使用起来很痛苦。在当前的标准库中,实际上有许多不同的数组类型。其中包括完全持久的数组、IO Monad的可变数组、ST Monad的可变数组以及上述的未装箱版本。有关更多信息,请访问haskell wiki好的。矢量是一个"更好"的数组
Data.Vector包在更高级别和更干净的API中提供了所有的数组优势。除非你真的知道你在做什么,否则如果你需要类似数组的性能,你应该使用这些。当然,有些警告仍然适用——可变数组(比如数据结构)在纯惰性语言中不太好用。不过,有时您希望O(1)的性能,而Data.Vector以一个可用的包提供给您。好的。你还有其他选择
如果您只需要能够在末尾有效插入的列表,则可以使用差异列表。列表混乱性能的最好例子往往来自[Char],前奏曲的别名是String。Char列表很方便,但运行速度比c字符串慢20倍,因此可以随意使用Data.Text或非常快的Data.ByteString。我敢肯定还有其他我现在没有想到的面向序列的库。好的。结论
在haskell列表中,90%以上的时间我需要一个顺序收集是正确的数据结构。列表类似于迭代器,使用列表的函数可以很容易地使用它们附带的toList函数与其他任何数据结构一起使用。在一个更好的世界里,前奏曲将完全是关于它使用的容器类型的参数化,但是目前[]已经抛弃了标准库。所以,使用列表(几乎)每一个地方肯定是好的。您可以获得大多数列表函数的完全参数化版本(并且可以使用它们)。好的。
事实上,Data.Traversable定义了一个API,它或多或少在任何"类似列表"的事物上都是通用的。好的。
不过,尽管您可以很好地编写完全参数化的代码,但我们中的大多数人都不是,而且到处都在使用列表。如果你在学习,我强烈建议你也这样做。好的。
编辑:根据评论,我意识到我从未解释何时使用Data.Vector与Data.Sequence。数组和向量提供了极快的索引和切片操作,但从根本上说是暂时(必需)的数据结构。像Data.Sequence和[]这样的纯功能数据结构可以有效地从旧值中生成新值,就像修改了旧值一样。好的。
1
| newList oldList = 7 : drop 5 oldList |
号
不修改旧列表,也不必复制它。所以即使oldList非常长,这个"修改"也会非常快。类似地好的。
1
| newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence |
将产生一个新的序列,用一个newValue代替它的3000个元素。同样,它不会破坏旧序列,只会创建一个新序列。但是,它非常有效地做到了这一点,取o(log(min(k,k-n)),其中n是序列的长度,k是您修改的索引。好的。
用Vectors和Arrays不容易做到这一点。它们可以被修改,但这是真正必要的修改,所以不能在常规的haskell代码中完成。这意味着在Vector包中进行修改的操作,如snoc和cons必须复制整个向量,因此需要O(n)时间。唯一的例外是,您可以在STmonad(或IO)中使用可变版本(Vector.Mutable),并像在命令式语言中那样进行所有修改。完成后,您可以"冻结"向量,将其转换为要与纯代码一起使用的不可变结构。好的。
我的感觉是,如果列表不合适,你应该默认使用Data.Sequence。只有当您的使用模式不涉及进行许多修改,或者您需要在ST/IO单体中具有极高的性能时,才使用Data.Vector。好的。
如果所有这些关于STMonad的谈论都让你困惑:那么,你就更应该坚持纯快速而美丽的Data.Sequence。好的。好啊。
- 我听到的一个见解是,列表基本上和Haskell中的数据结构一样是一种控制结构。这是有意义的:在不同语言中使用C样式的for循环时,在haskell中使用[1..]列表。列表也可以用于像回溯这样有趣的事情。把它们当作控制结构(有点)真的有助于理解它们是如何被使用的。
- @Tikhonjelvis不错,"和数据结构一样是一个控制结构",它捕获了关于如何使用列表的一些基本信息。
- 非常感谢你的努力。回答得很好。:)
- 回答得很好。我唯一的抱怨是,"序列是功能性的",是有点低估了它们。序列是有功能的冬虫夏草酱。他们的另一个好处是快速加入和拆分(log n)。
- @丹伯顿集市。我可能卖不到Data.Sequence。手指树是计算机史上最伟大的发明之一(Guibas可能有一天会获得图灵奖),Data.Sequence是一个优秀的实现,有一个非常有用的API。
- @PhilipJF,使用data.sequence比data.vector有哪些优点和缺点?向量也有类似列表的控制结构和迭代器,但似乎具有数组的数据位置优势。我在代码中使用了向量和列表,但没有使用序列。我一定是错过了一些序列提供的列表和向量不能。
- @Sal我更新了我的帖子以回应你的评论
- "usedata.vector仅当您的使用模式不涉及进行许多修改,或者如果您需要在st/io monads中具有极高的性能……"有趣的措辞,因为如果您正在进行许多修改(例如重复(100k次)演化100k元素),那么您确实需要st/io vector来获得可接受的性能,
- 流融合部分缓解了对(纯)向量和复制的担忧,例如:import qualified Data.Vector.Unboxed as VU; main = print (VU.cons 'a' (VU.replicate 100 'b'))编译为核心:hpaste.org/65015中单个404字节(101个字符)的分配
- @Tikhonjelvis:那是因为名单是单子。
- 与这些其他结构相比,Data.IntMap的运行情况如何?在我看来,IntMap主要用于稀疏键。对吗?
- 要么江户记1〔6〕似乎已经不见了,要么我搜得很烂。哪一种情况?
- @jandvorak vector位于黑客攻击平台和平台的一部分。它不在基地,所以你必须在胡言乱语中使用+vector,这很烦人。你也可以使用hayo,它可以在基地之外搜索。
- 我相信连接数据。序列是O(log n),而不是O(1)。
- 请告诉我在哪里可以找到更多关于这个答案中出现的?符号的信息,当然,这与计算机科学有关。
- @F:这是大O符号。你可以在wikipedia en.wikipedia.org/wiki/big ou o ou notation上阅读,也可以在这里搜索。
- 对于较新的GHC版本,Prelude.map和类似的功能已经被概括为使用更通用的类型签名。