1. 程式人生 > >Scala 集合(一)

Scala 集合(一)

Scala 序列、對映和集合

1 序列

許多資料結構是序列型的,也就是說,元素可以按特定的順序訪問,如:元素的插入順
序或其他特定順序。collection.Seq是一個trait,是所有可變或不可變序列型別的抽象,其子trait collection.mutable.
Seq及collection.immutable.Seq分別對應可變和不可變序列。

列表也是一種序列,是函數語言程式設計中最常用的資料結構,從第一代函式式語言Lisp 就開始用列表了。

通常,向列表裡追加元素時,該元素會被追加到列表的開頭,成為新列表的“頭部”。除了頭部,剩下的部分就是原列表的元素,這些元素並沒有被修改,它們變成了新列表的“尾部”。圖中有兩個列表,List(1,2,3,4,5) 和List(2,3,4,5),後者是前者的尾部。

我們從舊列表中建立新列表的操作是O(1)。新列表的尾部與舊列表相同,只是在頭部多了一個新元素。在函式式資料結構的思想中,我們將複製、共享資料結構的開銷降到最低,這是第一個例子。為了支援可變性,我們必須要有降低複製開銷的能力。其他程式碼訪問原佇列時對新佇列是無感知的。列表是不可變的,因此在另一個執行緒中,建立新佇列的程式碼不會給訪問舊列表的程式碼帶來不可預料的修改操作。從佇列的建立過程,我們可以清楚地知道,計算佇列長度的操作是O(N),其他需要從頭遍歷佇列的操作也是如此。


object Test extends App {
 val l1 = List("aaaa","bbbb","cccc")
 val l2 = "dddd"::"eeee"::"ffff"::l1
  val l3 = "dddd"::"eeee"::"ffff"::Nil
  println(l1)
  println(l2)
  println(l3)
}

你可以用List.apply 方法建立佇列,然後用:: 方法(稱為cons,意為構造)向佇列頭部追加資料,從而建立新的列表。在這裡我們使用了簡單寫法,省略了點號與小括號。我們提到過,以冒號(:)結尾的方法向右結合,因此x::list 其實是list.::(x)。

++ 方法可以將兩個列表連線起來:

object Test extends App {
 val l1 = List("aaaa","bbbb","cccc")
 val l2 = "dddd"::"eeee"::"ffff"::l1
  val l3 = "dddd"::"eeee"::"ffff"::Nil
 val l4 = l1++l3
 println(l4)
}

輸出:
List(aaaa, bbbb, cccc, dddd, eeee, ffff)

在需要的時候構造列表的確是常用的做法,但不推薦方法將列表作為引數或作為返回值。而應該用Seq,這樣的話,Seq 的任何子型別就都可以用了,包括List 和Vector。

Seq 的構造方法是+: 而不是::。以下是前文使用過的一個例子,其中用到了Seq。注意,當你對伴隨物件使用Seq.apply 方法時,將創建出一個List,這是因為Seq 只是一個特徵,而不是具體的類。

在這裡,我們用Seq.empty 為seq3 和seq4 建立了空的佇列作為隊尾。在Scala 中,大部分集合型別的伴隨物件都使用empty 方法來建立該型別的空例項,類似列表的Nil 例項。

序列型別還定義了:+ 和+: 方法。它們有什麼區別,怎麼記住哪個是哪個呢?只要記住:總是靠近集合型別就可以了,

比如:list :+ x,x +: list。所以,:+ 方法用於在尾部追加元素,+: 方法用於在頭部追加元素:

Scala 定義的List 是不可變的,不過,它還定義了其他可變的列表型別,如ListBuffer和MutableList。只有當必須
修改元素時才可以使用可變型別。

雖然List 對於序列型別是個不錯的選擇,你也可以考慮用immutable.Vector 代替List,因為immutable.Vector的所有操作都是O(1) (常數時間),而List 對於那些需要訪問頭部以外元素的操作,都需要O(n) 操作。

我們能夠以常數時間複雜度獲取任意元素:

在結束對序列的討論之前,你還需要知道一個實現上的細節問題。為了鼓勵程式設計師使用不可變的集合型別,Predef 及Predef 中使用的其他型別在暴露部分不可變集合型別時,不需要顯式匯入或使用全路徑匯入。如:List 和Map。

2 對映表

對映表用來儲存鍵值對,但不應將其與很多資料結構的map 方法混淆。對映表與map 方法有一定程度的類似,前者每個鍵都對應一個值,後者每個輸入元素都產生一個輸出元素。

object Test extends App {
  val stateCapitals = Map("Alabama" -> "Montgomery", "Alaska" -> "Juneau", "Wyoming" -> "Cheyenne")
  val lengths = stateCapitals map {
    kv => (kv._1, kv._2.length)
  }
  val caps = stateCapitals map {
    case (k, v) => (k, v.toUpperCase)
  }

  val stateCapitals2 = stateCapitals + ( "Virginia" -> "Richmond")
  val stateCapitals3 = stateCapitals2 + ("New York" -> "Albany", "Illinois" -> "Springfield")
}

key -> value 的語法形式實際上是用庫中的隱式轉換實現的,實際呼叫了Map.apply 方法。Map.apply 方法的引數為一個兩元素的元組(鍵值對)。

map 的引數是一個函式,以上示例展示了定義這個函式的兩種方法。每個鍵值對(元組)都將被傳入到該函式中。我們可以將其引數定義為一個兩元素的元組,或使用類似case 語法從元組中提取鍵和值。

用+ 向Map 中新增一個或多個鍵值對,特別提醒鍵值對的括號不可以去

與List 不同,Map 有可變和不可變兩種實現: 分別是scala.collection.immutable.Map[A,B] 與scala.collection.mutable.Map[A,B]。可變的實現需要顯式匯入,不可變的實現則已經用Predef 暴露出來了。兩種實現都定義了+ 和- 操作用於增加和移除元素;以及++ 和-- 操作來增加和移除Iterator 中定義的元素(Iterator 也可以換為其他集合、列表等)。

3 集合

集合是無序集合型別的一個例子,所以集合不是序列。集合同樣要求元素具有唯一性:

object Test extends App {
  val states = Set("Alabama", "Alaska", "Wyoming")
  val lengths = states map (st => st.length)
  val states2 = states + "Virginia"
  val states3 = states2 + ("New York", "Illinois")
}

類似Map,特徵scala.collection.Set只定義不可變操作的方法。對於具體的不可變集合和可變集合,分別定義派生的特徵scala.collection.immutable.Set和scala.collection.mutable.Set可變的版本需要顯式匯入,而不可變的版本在Predef 中已經匯入了。兩者都為增加和移除元素定義了+ 和- 操作;為Iterator(也可以為其他集合、列表等)中的增加和移除元素定義了++ 和-- 操作。