我正在学习Haskell,并阅读了几篇有关Haskell列表和(插入您的语言)数组的性能差异的文章。
作为学习者,我显然只使用列表,甚至没有考虑性能差异。我最近开始调查,发现Haskell中提供了许多数据结构库。
有人可以解释列表,数组,向量,序列之间的区别,而无需深入了解数据结构的计算机科学理论吗?
另外,是否有一些常见的模式可以使用一种数据结构代替另一种数据结构?
我还缺少其他任何形式的数据结构并且可能有用吗?
我正在学习Haskell,并阅读了几篇有关Haskell列表和(插入您的语言)数组的性能差异的文章。
作为学习者,我显然只使用列表,甚至没有考虑性能差异。我最近开始调查,发现Haskell中提供了许多数据结构库。
有人可以解释列表,数组,向量,序列之间的区别,而无需深入了解数据结构的计算机科学理论吗?
另外,是否有一些常见的模式可以使用一种数据结构代替另一种数据结构?
我还缺少其他任何形式的数据结构并且可能有用吗?
Answers:
到目前为止,Haskell中用于顺序数据的最友好的数据结构是List
data [a] = a:[a] | []
列表为您提供ϴ(1)缺点和模式匹配。标准库,并为此事的序幕,充满了有用的列表功能应该垃圾代码(foldr
,map
,filter
)。列表是持久性的,也就是纯功能性的,这非常好。Haskell列表并不是真正的“列表”,因为它们是共性的(其他语言称为这些流),因此类似
ones :: [Integer]
ones = 1:ones
twos = map (+1) ones
tenTwos = take 10 twos
工作出色。无限的数据结构无处不在。
Haskell中的列表提供了一个界面,就像命令式语言中的迭代器一样(由于懒惰)。因此,它们被广泛使用是有道理的。
列表的第一个问题是索引到列表(!!)
需要ϴ(k)时间,这很烦人。此外,附加操作可能很慢++
,但是Haskell的惰性评估模型意味着,即使这些操作完全发生,也可以将其视为完全摊销。
列表的第二个问题是它们的数据本地性差。当内存中的对象没有彼此相邻布置时,实际处理器会产生高常量。因此,在C ++ std::vector
中,“ snoc”(末尾放入对象)的速度比我所知道的任何纯链表数据结构都要快,尽管这并不是一个持久化的数据结构,它没有Haskell的列表那么友好。
列表的第三个问题是它们的空间效率很差。一堆额外的指针会增加您的存储空间(增加一个常数)。
Data.Sequence
在内部基于手指树(我知道,您不想知道这一点),这意味着它们具有一些不错的属性
Data.Sequence
是一个完全持久的数据结构。Data.Sequence
,最多是恒定不变的。 另一方面, Data.Sequence
对于数据局部性问题并没有多大帮助,仅适用于有限集合(不如列表慢)
数组是CS中最重要的数据结构之一,但它们与惰性纯函数世界并不十分吻合。数组使ϴ(1)可以访问集合的中部,并且可以提供非常好的数据局部性/恒定因素。但是,由于它们不太适合Haskell,因此使用起来很麻烦。当前标准库中实际上有许多不同的数组类型。这些包括完全持久性数组,用于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
它们附带的其他任何数据结构一起使用。在一个更好的世界中,前奏将完全取决于其使用的容器类型的参数,但目前却 []
乱丢了标准库。因此,几乎可以在任何地方使用列表(几乎)。
您可以获得大多数列表函数的完全参数化版本(并且很荣幸使用它们)
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。
尽管如此,尽管您可能会很好并且只编写完全参数化的代码,但我们大多数人都不是,并且在各处使用list。如果您正在学习,我强烈建议您也这样做。
编辑:根据意见,我意识到,我从来没有解释何时使用Data.Vector
VS Data.Sequence
。数组和向量提供了极快的索引和切片操作,但从根本上讲是瞬态(命令式)数据结构。纯功能性数据结构一样Data.Sequence
,并[]
让有效地产生新的,如果你修改了旧值从旧值值。
newList oldList = 7 : drop 5 oldList
不会修改旧列表,也不必复制它。因此,即使oldList
时间长得令人难以置信,这种“修改”也会非常快。相似地
newSequence newValue oldSequence = Sequence.update 3000 newValue oldSequence
将产生一个新的序列,用newValue
for代替其3000元素。同样,它不会破坏旧序列,只会创建一个新序列。但是,这样做非常有效,采用O(log(min(k(k,kn))),其中n是序列的长度,而k是您修改的索引。
您无法轻松地使用Vectors
和进行此操作Arrays
。可以修改它们,但这是真正的命令性修改,因此不能用常规的Haskell代码完成。这意味着Vector
包中的操作需要进行修改,例如复制snoc
,cons
必须复制整个向量,因此需要花费O(n)
时间。唯一的例外是,您可以Vector.Mutable
在ST
monad(或IO
)中使用可变版本(),并像使用命令式语言一样进行所有修改。完成后,您可以“冻结”向量以将其变成要与纯代码一起使用的不变结构。
我的感觉是,Data.Sequence
如果列表不合适,则应默认使用。使用Data.Vector
仅当您使用模式不涉及使许多修改,或者如果你需要的ST / IO单子内极高的性能。
如果所有关于ST
monad的谈话都让您感到困惑:那就更有理由坚持纯净而又美丽了Data.Sequence
。
[1..]
Haskell中的列表。列表还可以用于有趣的事情,例如回溯。将它们视为控制结构(某种)确实有助于理解它们的用法。
Data.Sequence
。手指树是计算历史上最了不起的发明之一(Guibas也许有一天应该会获得图灵奖),并且Data.Sequence
是一种出色的实现,并且具有非常有用的API。
import qualified Data.Vector.Unboxed as VU; main = print (VU.cons 'a' (VU.replicate 100 'b'))
在Core:hpaste.org/65015