Haskell:列表,数组,向量,序列

Haskell: Lists, Arrays, Vectors, Sequences

我正在学习haskell,并阅读了一些关于haskell列表和(插入您的语言)数组的性能差异的文章。

作为一个学习者,我显然只使用列表,而不考虑性能差异。我最近开始调查,发现哈斯克尔有许多可用的数据结构库。

有人能在不深入研究数据结构的计算机科学理论的情况下,解释列表、数组、向量和序列之间的区别吗?

此外,在使用一种数据结构而不是另一种数据结构时,是否有一些常见的模式?

是否有其他形式的数据结构是我所缺少的,并且可能有用?


摇滚乐列表

到目前为止,haskell中顺序数据最友好的数据结构是列表好的。

1
 data [a] = a:[a] | []

列表提供&1012;(1)缺点和模式匹配。标准库,也就是序曲,充满了有用的列表函数,这些函数应该丢弃您的代码(foldrmapfilter。列表是持久的,也就是纯功能的,非常好。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],前奏曲的别名是StringChar列表很方便,但运行速度比c字符串慢20倍,因此可以随意使用Data.Text或非常快的Data.ByteString。我敢肯定还有其他我现在没有想到的面向序列的库。好的。结论

    在haskell列表中,90%以上的时间我需要一个顺序收集是正确的数据结构。列表类似于迭代器,使用列表的函数可以很容易地使用它们附带的toList函数与其他任何数据结构一起使用。在一个更好的世界里,前奏曲将完全是关于它使用的容器类型的参数化,但是目前[]已经抛弃了标准库。所以,使用列表(几乎)每一个地方肯定是好的。您可以获得大多数列表函数的完全参数化版本(并且可以使用它们)。好的。

    1
    2
    3
    4
    Prelude.map                --->  Prelude.fmap (works for every Functor)
    Prelude.foldr/foldl/etc    --->  Data.Foldable.foldr/foldl/etc
    Prelude.sequence           --->  Data.Traversable.sequence
    etc

    事实上,Data.Traversable定义了一个API,它或多或少在任何"类似列表"的事物上都是通用的。好的。

    不过,尽管您可以很好地编写完全参数化的代码,但我们中的大多数人都不是,而且到处都在使用列表。如果你在学习,我强烈建议你也这样做。好的。

    编辑:根据评论,我意识到我从未解释何时使用Data.VectorData.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是您修改的索引。好的。

    VectorsArrays不容易做到这一点。它们可以被修改,但这是真正必要的修改,所以不能在常规的haskell代码中完成。这意味着在Vector包中进行修改的操作,如snoccons必须复制整个向量,因此需要O(n)时间。唯一的例外是,您可以在STmonad(或IO)中使用可变版本(Vector.Mutable),并像在命令式语言中那样进行所有修改。完成后,您可以"冻结"向量,将其转换为要与纯代码一起使用的不可变结构。好的。

    我的感觉是,如果列表不合适,你应该默认使用Data.Sequence。只有当您的使用模式不涉及进行许多修改,或者您需要在ST/IO单体中具有极高的性能时,才使用Data.Vector。好的。

    如果所有这些关于STMonad的谈论都让你困惑:那么,你就更应该坚持纯快速而美丽的Data.Sequence。好的。好啊。