ChatGPT解决这个技术问题 Extra ChatGPT

什么时候应该在 Scala 中选择 Vector?

Vector 似乎迟到了 Scala 收藏派对,所有有影响力的博文都已经离开了。

在 Java 中,ArrayList 是默认集合 - 我可能会使用 LinkedList,但前提是我已经考虑了算法并足够注意进行优化。在 Scala 中,我应该使用 Vector 作为我的默认 Seq,还是尝试找出 List 实际上更合适的时间?

我想我的意思是,在 Java 中我会创建写 List<String> l = new ArrayList<String>() Scala 博客会让你相信每个人都使用 List 来获得持久的集合优点 - 但是 Vector 足够通用,我们应该在 List 中使用它地方?
@Debilski:我想知道你的意思是什么。当我在 REPL 中输入 Seq() 时,我得到一个 List
嗯,好吧,它在文档中是这样说的。也许这仅适用于 IndexedSeq
关于 Seq 的默认具体类型的评论已有三年多的历史了。从 Scala 2.11.4(及更早版本)开始,Seq 的默认具体类型是 List
对于随机访问,向量更好。对于头部、尾部访问,列表更好。对于批量操作,例如 map,filter,vector 是首选,因为 vector 由 32 个元素组成一个块,而 list 用指针组织元素,不能保证这些元素彼此接近。

s
simbo1905

作为一般规则,默认使用 Vector。对于几乎所有内容,它都比 List 快,对于大于平凡大小的序列,它的内存效率更高。请参阅此 documentation,了解 Vector 与其他集合相比的相对性能。使用 Vector 有一些缺点。具体来说:

头部的更新比 List 慢(虽然没有你想象的那么快)

Scala 2.10 之前的另一个缺点是对 List 的模式匹配支持更好,但在 2.10 中使用通用的 +::+ 提取器进行了纠正。

还有一种更抽象的代数方式来解决这个问题:你在概念上有什么样的序列?另外,您在概念上在用它做什么?如果我看到一个返回 Option[A] 的函数,我知道该函数在其域中有一些漏洞(因此是部分漏洞)。我们可以将相同的逻辑应用于集合。

如果我有一个 List[A] 类型的序列,我实际上是在断言两件事。首先,我的算法(和数据)完全是堆栈结构的。其次,我断言我将对这个集合做的唯一事情是完整的 O(n) 遍历。这两个真的是齐头并进。相反,如果我有 Vector[A] 类型的东西,我要断言的唯一是我的数据具有明确定义的顺序和有限长度。因此,Vector 的断言较弱,这导致其更大的灵活性。


2.10 出来有一段时间了,List 模式匹配还是比 Vector 好?
列表模式匹配不再更好。事实上,情况恰恰相反。例如,要获得头部和尾部,可以执行 case head +: tailcase tail :+ head。要与空匹配,您可以执行 case Seq() 等等。您需要的一切都在 API 中,它比 List 更通用
List 使用单链表实现。 Vector 的实现类似于 Java 的 ArrayList
@JosiahYoder 它的实现与 ArrayList 完全不同。 ArrayList 包装了一个动态调整大小的数组。 Vector 是 trie,其中键是值的索引。
我道歉。我正在访问一个对细节模糊不清的网络资源。我应该更正我之前的陈述吗?还是那是不好的形式?
D
Daniel C. Sobral

好吧,如果算法可以单独使用 ::headtail 实现,那么 List 可以非常快。我最近有一个对象教训,当时我通过生成 List 而不是 Array 击败了 Java 的 split,并且无法用其他任何东西击败它。

但是,List 有一个基本问题:它不适用于并行算法。我无法以有效的方式将 List 拆分为多个段,或将其连接回来。

还有其他类型的集合可以更好地处理并行性 - Vector 就是其中之一。 Vector 还具有很好的局部性 - List 没有 - 这对于某些算法来说可能是一个真正的优势。

因此,考虑到所有因素,Vector 是最佳选择除非您有特定的考虑使其他集合之一更可取 - 例如,如果您想要惰性评估,您可以选择 Stream 并且缓存(Iterator 更快,但不缓存),或者 List 如果算法是通过我提到的操作自然实现的。

顺便说一句,最好使用 SeqIndexedSeq,除非您需要特定的 API(例如 List::),或者如果您的算法可以使用 GenSeqGenIndexedSeq并行运行。


感谢你的回答。您所说的“具有很大的局部性”是什么意思?
@ngocdaothanh 这意味着数据在内存中紧密组合在一起,从而提高了数据在需要时位于缓存中的机会。
@user247077 是的,鉴于我提到的细节,列表可以在性能上击败向量。并非所有向量的动作都摊销 O(1)。事实上,在不可变数据结构上(就是这种情况),任一端的交替插入/删除根本不会摊销。在这种情况下,缓存是无用的,因为您总是在复制向量。
@user247077 也许您不知道 Vector 是 Scala 中的不可变数据结构?
@ user247077 它比这更复杂,包括一些内部可变的东西以使附加更便宜,但是当您将它用作堆栈时,这是不可变列表的最佳方案,您最终仍然具有与链表相同的内存特性,但是具有更大的内存分配配置文件。
d
dth

这里的一些陈述令人困惑甚至是错误的,尤其是 Scala 中的 immutable.Vector 类似于 ArrayList 的想法。 List 和 Vector 都是不可变的、持久的(即“获得修改后的副本很便宜”)数据结构。没有合理的默认选择,因为它们可能适用于可变数据结构,但这取决于您的算法在做什么。 List 是一个单链表,而 Vector 是一个 base-32 整数 trie,即它是一种具有 32 度节点的搜索树。使用这种结构,Vector 可以相当快地提供最常见的操作,即在 O(log_32( n))。这适用于头/尾中的前置、附加、更新、随机访问、分解。顺序迭代是线性的。另一方面,列表仅提供线性迭代和恒定时间前置,头/尾分解。其他一切都需要一般的线性时间。

这看起来好像 Vector 在几乎所有情况下都是 List 的一个很好的替代品,但是前置、分解和迭代通常是函数式程序中序列上的关键操作,并且这些操作的常数对于向量来说要高得多,因为到其更复杂的结构。我做了一些测量,所以列表的迭代速度大约是两倍,列表上的 prepend 大约快 100 倍,列表上的头/尾分解大约快 10 倍,并且从可遍历的生成向量大约快 2 倍。 (这可能是因为 Vector 可以在使用构建器构建它时一次分配 32 个元素的数组,而不是一个一个地添加或附加元素)。当然,所有在列表上花费线性时间但在向量上实际上是恒定时间的操作(作为随机访问或追加)在大型列表上将非常缓慢。

那么我们应该使用哪种数据结构呢?基本上,有四种常见情况:

我们只需要通过映射、过滤、折叠等操作来转换序列:基本上没关系,我们应该对我们的算法进行通用编程,甚至可以从接受并行序列中受益。对于顺序操作 List 可能要快一些。但是如果你必须优化,你应该对它进行基准测试。

我们需要大量的随机访问和不同的更新,所以我们应该使用向量,列表会非常慢。

我们以经典的函数方式对列表进行操作,通过递归分解的前置和迭代来构建它们:使用列表,向量将慢 10-100 倍或更多。

我们有一个性能关键算法,它基本上是命令式的,并且对列表进行大量随机访问,例如就地快速排序:在本地使用命令式数据结构,例如 ArrayBuffer,并从中复制数据。


L
Luigi Plinge

对于不可变集合,如果您想要一个序列,您的主要决定是使用 IndexedSeq 还是 LinearSeq,这为性能提供了不同的保证。 IndexedSeq 提供元素的快速随机访问和快速长度操作。 LinearSeq 仅通过 head 提供对第一个元素的快速访问,但也具有快速 tail 操作。 (取自 Seq 文档。)

对于 IndexedSeq,您通常会选择 VectorRangeWrappedString 也是 IndexedSeq。

对于 LinearSeq,您通常会选择 List 或其惰性等效项 Stream。其他示例是 QueueStack

因此,在 Java 术语中,ArrayList 的使用与 Scala 的 Vector 类似,LinkedList 与 Scala 的 List 类似。但在 Scala 中,我倾向于比 Vector 更频繁地使用 List,因为 Scala 对包括遍历序列的函数有更好的支持,比如映射、折叠、迭代等。你会倾向于使用这些函数来操作列表作为整体,而不是随机访问单个元素。


但是,如果 Vector 的迭代速度比 List 的快,并且我也可以映射 fold 等,那么除了一些特殊情况(基本上所有专门用于 List 的 FP 算法)之外,List 似乎本质上是遗留的。
@Duncan 您在哪里听说 Vector 的迭代速度更快?首先,您需要跟踪和更新当前索引,而链表不需要这样做。我不会将列表函数称为“特殊情况”——它们是函数式编程的基础。不使用它们就像尝试在没有 for 或 while 循环的情况下编写 Java。
我很确定 Vector 的迭代 更快,但需要有人对其进行基准测试才能确定。
我认为 Vector 中的 (?) 元素以 32 个一组物理地一起存在于 RAM 中,它们更完全适合 CPU 缓存......因此缓存未命中的情况更少
D
Debilski

在涉及大量随机访问和随机突变的情况下,Vector(或 - 正如 docs 所说 - Seq)似乎是一个很好的折衷方案。这也是 performance characteristics 所建议的。

此外,Vector 类似乎在没有太多数据重复的分布式环境中运行良好,因为不需要对完整对象执行写时复制。 (见:http://akka.io/docs/akka/1.1.3/scala/stm.html#persistent-datastructures


有很多东西要学... Vector 作为默认 Seq 是什么意思?如果我写 Seq(1, 2, 3),我会得到 List[Int] 而不是 Vector[Int]。
如果您有随机访问权限,请使用 IndexedSeq。这也是 Vector,但这是另一回事。
@DuncanMcGregor:向量是实现 Seq 的默认 IndexedSeqSeq(1, 2, 3) 是使用 List 实现的 LinearSeq
J
Joshua Hartman

如果您的编程是不可变的并且需要随机访问,那么 Seq 就是您要走的路(除非您想要一个 Set,而您实际上经常这样做)。否则 List 工作得很好,除了它的操作不能并行化。

如果您不需要不可变的数据结构,请坚持使用 ArrayBuffer,因为它是 Scala 等价于 ArrayList。


我坚持不可变的、持久的集合领域。我的观点是,即使我不需要随机访问,Vector 是否有效地取代了 List?
取决于用例。向量更加平衡。迭代比列表快,随机访问要快得多。更新速度较慢,因为它不仅仅是一个列表前置,除非它是可以使用构建器完成的折叠的批量更新。也就是说,我认为 Vector 是最好的默认选择,因为它用途广泛。
我认为这是我问题的核心——向量非常好,我们不妨在示例通常显示列表的地方使用它们。