为什么在Scala List前面添加一个常量时间操作,但是附加一个线性时间操作?

时间:2014-12-03 17:36:37

标签: scala

我现在正在从Odersky的Scala编程'编程中学习Scala。我刚看完这个

  

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

为什么在线性时间附加到附加到列表但是只有恒定的时间来预先添加到列表中。我目前的猜测是,它以某种方式在内部实现为链接列表,这将解释这两个操作之间的差异。在这种情况下,如何实现具有恒定时间附加的ListBuffers?

2 个答案:

答案 0 :(得分:12)

这里有两个问题:一个是关于列表的非常基本的问题,另一个是scala特定的问题。首先,我会画一幅画。 “列表”抽象数据类型是计算机科学中最古老的数据类型之一;它可以追溯到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 

这样做的好处在于它保留了所有现有的参考文献,保留了不变性。但是,如果我们想要/追加/到列表,我们必须将对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,它的附加工作通过直接改变后备列表,重写最终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具有可见性私有[scala],允许不安全的“幕后”操作来改变集合,例如ListBuffer。

答案 1 :(得分:8)

它们的实现方式与链接列表类似。不同之处在于ListBuffer还包含指向列表尾部的指针。 Scala的源代码是开放的,如果您对它的细节感到好奇,可以在github上进行探索(例如,这里是ListBuffer的附加代码)