【问题标题】:Why exactly is prepend to a Scala List a constant time operation, but append a linear time operation?为什么在 Scala List 之前添加一个恒定时间操作,但添加一个线性时间操作?
【发布时间】:2014-12-03 17:36:36
【问题描述】:

我现在正在从 Odersky 的“Scala 编程”中学习 Scala,我刚刚阅读了这篇文章

...追加到列表所需的时间随着列表的大小线性增长 列表,而在前面加上 :: 需要固定的时间...

为什么添加到列表需要线性时间,而添加到列表需要恒定时间。我目前的猜测是它在内部以某种方式实现为链表,这将解释这两个操作之间的区别。在那种情况下,具有恒定时间追加的 ListBuffers 是如何实现的?

【问题讨论】:

  • 列表缓冲区可以在恒定时间内追加,因为它们会改变 cdr 指针。但是,公共列表 API 不允许这样做。
  • 追加,需要遍历整个列表。
  • 如果我的回答满足了你的问题,你能把它打勾为完成吗?否则,请添加评论,说明您还需要知道什么:)

标签: scala


【解决方案1】:

这里有两个问题:一个是关于列表的非常基本的问题,另一个是 scala 特定的。首先,我会画一张图。 “List”抽象数据类型是计算机科学中最古老的数据类型之一;它可以追溯到 1956 年,也是 LISP 语言的第一个实现。

列表 ADT 确实是使用单向链表实现的。每个元素传统上称为一个 cons 单元格(Nil 表示列表结尾):

+------+------+      +------+------+    +--------+
|   1  |   * -+----> |  3   +   * -+--->|  NIL   |
+------+------+      +-------------+    +--------+
^
|
val list1 

这对应于 scala 中的链式“cons”(列表构造)调用:

scala> val list1 = 1 :: 3 :: Nil
list1: List[Int] = List(3, 1)

List 是一个不可变的数据结构。我们不能通过重写任何内容来破坏性地更新它 现有的指针/引用。那么如果我们想在前面添加呢?

scala> val list2 = 0 :: list1
list2: List[Int] = List(0, 1, 3)

创建 list2 非常快速和简单,我们只需创建一个新的 cons 单元格并将其添加到现有列表之前:

+------+------+      +------+------+      +------+------+    +--------+
|   0  |   *--+----->|   1  |   * -+----> |  3   +   * -+--->|  NIL   |
+------+------+      +------+------+      +-------------+    +--------+
 ^                   ^
 |                   |
 val list2           val list1 

这样做的美妙之处在于它保持所有现有引用不变,保持不变性。但是,如果我们想 /append/ 到列表中,我们必须将 Nil 的引用覆盖到一个新元素,从而改变 list1。因此,唯一的方法是复制整个现有列表并重写它:

scala> val list3 = list1 ++ (5 :: Nil)
list3: List[Int] = List(1, 3, 5)


+------+------+      +------+------+      +------+------+    +--------+
|   0  |   *--+----->|   1  |   * -+----> |  3   +   * -+--->|  NIL   |
+------+------+      +------+------+      +-------------+    +--------+
^                    ^                                           ^
|                    |                                           |
val list2            val list1                                   |
                                                                 |
                                                                 |
(copy)                                                           |
+------+------+      +------+------+    +--------+-------+       |
|   1  |   * -+----> |  3   +   * -+--->|   5    |    *--+-------+
+------+------+      +-------------+    +--------+-------+    
^
|
val list3 

这是一个线性时间操作,因为要附加到长度为 N 的列表,我们必须复制所有 N 个 cons 单元格(Nil 除外)。

至于 scala 的 ListBuffer,它的 append 通过直接改变后备列表、重写最终 cons 单元格的尾部引用来工作,该单元格的类型看起来有点尴尬::[A]。

在 List.scala 中:

final case class ::[B](override val head: B, private[scala] var tl: List[B]) extends List[B] {
   override def tail : List[B] = tl
   override def isEmpty: Boolean = false
}

[..] 在 ArrayList.scala 中:

/** Appends a single element to this buffer. This operation takes constant time.
*
*  @param x  the element to append.
*  @return   this $coll.
*/
def += (x: A): this.type = {
  if (exported) copy()
  if (isEmpty) {
    last0 = new :: (x, Nil)
    start = last0
  } else {
    val last1 = last0
    last0 = new :: (x, Nil)
    last1.tl = last0
  }
  len += 1
  this
} 

last1.tl = last0 是允许的,因为 ::.tl 具有可见性 private[scala],允许不安全的“幕后”操作来改变集合,例如 ListBuffer。

【讨论】:

  • +1 表示 prepend 的不变性与 append 的可变性。提供了关于不变性成本的重要课程。另外,我不确定为什么这个答案没有被标记为正确而不是前一个。它显然更加详细和有用。
【解决方案2】:

它们的实现都类似于链表。不同之处在于 ListBuffer 还包含一个指向列表尾部的指针。 Scala的源代码是开放的,如果你对它的细节感兴趣,可以在github上探索它(例如,这里是ListBuffer的附加代码)

【讨论】:

    猜你喜欢
    • 2015-08-10
    • 1970-01-01
    • 2019-06-02
    • 2020-11-19
    • 1970-01-01
    • 2015-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多