2014-12-03 23 views
1

我從Odersky的的「在Scala編程」學習Scala現在的中間,我剛剛看了這個爲什麼要在Scala List中添加一個常量時間操作,但追加了一個線性時間操作?

...需要追加到列表中的時間與 的大小線性增長列表,而預先考慮::需要一個固定的時間...

爲什麼要花費線性時間來追加到列表上,而只有一個常量時間才能附加到列表上。我目前的猜測是它以某種方式作爲鏈接列表在內部實施,這將解釋兩種操作之間的差異。在那種情況下,如何實現具有恆定時間的ListBuffers?

+0

列表緩衝區可以在常量時間附加,因爲它們會改變cdr指針。然而,公開列表API不允許這樣做。 – rightfold 2014-12-03 17:42:51

+0

要追加,需要遍歷整個列表。 – 2014-12-03 17:43:57

+0

如果我的回答滿足您的問題,您是否可以勾選完成?否則請添加註釋,解釋你還需要知道的其他內容:) – 2014-12-09 08:22:17

回答

8

它們的實現方式與鏈接列表類似。區別在於ListBuffer還包含一個指向列表尾部的指針。 Scala的源代碼是開放的,如果您想了解它的細節,你可以探索它在github(例如,這裏是ListBuffer的追加代碼)

7

這裏有兩個問題:一個是關於列出一個很基本的一個,另一個是scala特有的。首先,我會畫一幅畫。 「List」抽象數據類型是計算機科學中最古老的一種;它回溯到1956年並且是LISP語言的首次實施。

列表ADT實際上是使用單向鏈表實現的。每個元件傳統上稱爲一個cons單元(無指示列表的結束):

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

這對應於鏈式「缺點」(列表構造)調用在階:

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

列表是一個不可變數據結構。我們不能通過重寫任何 現有的指針/引用來破壞性地更新它。那麼,如果我們想補充它呢?

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

創建列表2是非常快速和容易的,我們只需要創建一個新的利弊細胞和它前面加上現有的列表:

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

這樣做的好處是,它讓所有現有的引用不變,保持不變性。但是,如果我們想/ append /到列表中,我們將不得不將參數覆蓋到新元素,從而改變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中。階:

/** 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被允許的,因爲:: T1具有可視性私人[階],「發動機罩下」操作允許不安全的突變集合,如ListBuffer。

+2

+1對於prepend的不變性與append的可變性。爲不變性成本提供了一個很好的教訓。另外,我不知道爲什麼這個答案沒有標記爲正確的,而不是以前的答案。它顯然更詳細和有益。 – algorithmic 2015-05-15 18:51:00