【问题标题】:Why are Scala's `Lists` implemented as linked lists为什么Scala的`Lists`被实现为链表
【发布时间】:2011-07-05 01:05:03
【问题描述】:

我一直认为链表的好处是您可以添加或删除项目(尤其是从末尾),而无需复制大量元素,这要归功于指针的美感。

然而,Scala 的List 是不可变的(至少在默认情况下)。拥有一个不可变链表有什么好处(因为有明确的缺点,例如不是 O(1) 元素访问。)

谢谢!

【问题讨论】:

    标签: list scala linked-list immutability


    【解决方案1】:

    然而,Scala 的 List 是不可变的(至少默认情况下是这样)。拥有不可变链表有什么好处

    我在这里聚会有点晚了,但我觉得有一个重要的观点没有人提出:

    除了 Rex Kerr 所说的之外,值得指出的是,通过添加到不可变单链表的前面来构建新列表是一个常数时间操作。您只需创建一个指向现有列表的新节点。由于列表是不可变的,因此您知道新的尾部永远不会改变,并且您不需要复制任何数据。

    我怀疑,您可以构建新的、更大但仍然不可变的列表,并使用恒定的时间操作,并且内存占用很小(仅一个节点的成本),这是数据的很大一部分原因选择了结构。

    【讨论】:

      【解决方案2】:

      虽然我几乎不知道如何实现 scala。不过,应该避免使用 LinkedLists。

      它们在当前硬件上效率低下,因为遍历它们每个节点实际上会导致缓存未命中。 缓存未命中是当今控制性能的因素。

      【讨论】:

      • 性能不是特别重要的代码呢? (你认为那是几部分的代码?)
      • @Rex,我想我不明白这个问题。通过任何方法实现某些东西必须有背后的推理。我很确定 Scala 人参加了一个专门讨论在 JVM 上运行的非 Java 语言以提高性能的会议。
      • @bestsss - 我不明白你的推理。一些 Scala 开发人员关心性能,因此应该始终避免使用链表,因为它们可能会导致缓存未命中?嗯?也许在某些情况下,缓存未命中并不重要,也许人们可以关心两个不同的事情并希望两者都有好的解决方案
      • @bestsss - 我知道你的意思。但是这个事实与你关于链表的陈述完全无关。
      • @bestsss - 你能用来自特定 Scala 实现的一些性能测试用例的一些具体性能数字来支持你的“抽象”一般断言吗? IOW,你能用实际的性能测试结果证明你的观点的有效性来支持你大部分无上下文的断言吗?顺便说一句,我真的很想知道您是否正在做某事,因为我也有类似的担忧,但还没有足够的空闲时间来测试和验证/使我的推测无效。
      【解决方案3】:

      我认为主要原因是链表最强大的用途之一是头/尾拆分。有很多递归算法看起来像这样:

      def listlen[A](xs: List[A], already: Int = 0): Int = xs match {
        case Nil => already
        case x :: rest => listlen(rest, already+1)
      }
      

      当然,列表已经知道如何获取它们的长度;这只是一个例子。关键是你把头拿掉,然后用尾巴做其他事情——很多有用的事情都是这样工作的。

      由于列表是不可变的,我们可以做其他事情——我们可以花尽可能多的时间来评估列表的其余部分!我们不必一口气完成;我们确信它永远不会改变。如果列表是可变的,则情况并非如此——要么我们需要锁定列表,防止其他人看到它,要么我们需要制作整个事物的防御性副本,以防有人交换它。

      现在,如果您真的想要一个可变列表,mutable.LinkedList 具有您所说的良好插入属性。但这并不能让您获得不可变列表所提供的优雅递归。

      (请注意,您可以使用不可变的数组支持结构来完成其中的一些操作,但是包含每个元素的集合的可能好处是您不需要保存或复制整个数组,因为您需要一个最后的几个元素;如果列表中的早期元素不再被指向,它们可以被垃圾回收。)

      【讨论】:

      • LinkedList 在当今硬件上只是一种低效的数据结构,每个节点都是一次缓存未命中,没有什么比缓存未命中更昂贵的了。
      • @bestsss - 取决于上下文。通常,是的,你是对的。从性能的角度来看,肯定应该对链表可疑,除非它们使您可以轻松使用更有效的算法(例如“不需要制作庞大数据集的防御性副本” )。但是如今硬件速度如此之快,以至于许多任务都受到您正确编码能力的限制,而不是硬件快速运行它的能力。 (但在抱怨缓存未命中之前,我会批评链表是不可并行的。哦,页面错误仍然方式比缓存未命中更糟糕。)
      • @Rex,交换或访问任何 IO 会更慢;你知道我不是那个意思。选择 LinkedList 作为默认结构可能会对许多任务造成巨大的性能影响。我仅将链接结构与基于线性的结构进行比较。链接结构仅在遍历后插入中间有用。循环列表可以在两端添加而不会显着降低性能。此外,链接结构(包括 java.util.HashMap)的占用空间要大得多。
      • @bestsss - 我认为你没有抓住重点。有时不需要两端相加,有时不变性可以帮助你轻松编写更高效的算法。对于那些时候,或者当性能不如正确性重要时,List 是一个不错的选择。对于其他时候(例如,如果您必须在两端添加),您最好选择其他东西。
      • @bestsss - 重复 constail(与添加到堆栈和从堆栈中删除一样)很难有效地使用不可变数组支持的结构。
      【解决方案4】:

      scala.Listscala.collection.immutable.List 的别名。它只是众多具体的集合实现之一。

      如果您有 Java 背景,请将java.util.List 等同于scala.collection.mutable.Buffer,而不是scala.List

      您可以在Scala 2.8 Collections Overview. 中了解馆藏库的结构概览。在这里您会发现许多其他具体实现(可变和不可变)。

      【讨论】:

        【解决方案5】:

        结构共享。如果您对列表执行高阶函数(map、fold 等),您将返回一个共享前一个列表指针的列表的新实例。

        Daniel Spiewak 上周在 NE Scala 上做了一个关于函数式数据结构的精彩演讲。见这里:http://www.nescala.org/2011/

        【讨论】:

        • 为什么不能用不可变的java.util.ArrayList 之类的东西来做到这一点?
        • 我想您可以使用 Google 的 com.google.common.collect.ImmutableList,它与派生它的列表不共享结构。不过内存加倍。只要确保你不使用 Collections.unmodifiableList,它会返回一个可变列表的视图,其中的数据仍然可以更改。
        • @aharon A java.util.ArrayList 从来都不是一成不变的。您可以获得它的不可修改的视图,但原始参考仍然能够更改它,因此库不能依赖它永远不会改变。
        • 所以你制作了一个类似 ArrayList 的东西,它是可变的。 ArrayList 不是很高级,即使对于初学者来说也不会太难。
        猜你喜欢
        • 2012-08-16
        • 1970-01-01
        • 1970-01-01
        • 2011-02-25
        • 1970-01-01
        • 2012-11-29
        • 2023-03-19
        • 2017-06-24
        • 2020-09-25
        相关资源
        最近更新 更多