集合

具體不可變集合類別

語言

Scala 提供許多具體不可變集合類別供您選擇。它們在實作的特質(對應、集合、序列)、是否可以是無限的,以及各種運算的速度上有所不同。以下是 Scala 中使用的一些最常見的不可變集合類型。

清單

清單是一個有限的不可變序列。它們提供對其第一個元素以及清單其他部分的常數時間存取,並且它們有一個常數時間 cons 運算,用於將新元素新增到清單的前面。許多其他運算需要線性時間。

LazyLists

一個 LazyList 就像一個列表,只不過它的元素是延遲計算的。由於這個原因,一個 lazy list 可以無限長。只有那些請求的元素才會被計算。否則,lazy list 具有與列表相同的效能特徵。

列表是用 :: 算子建構的,而 lazy list 則是用看起來相似的 #:: 建構的。以下是包含整數 1、2 和 3 的 lazy list 的一個簡單範例

scala> val lazyList = 1 #:: 2 #:: 3 #:: LazyList.empty
lazyList: scala.collection.immutable.LazyList[Int] = LazyList(<not computed>)

這個 lazy list 的頭部是 1,尾部是 2 和 3。不過,這裡沒有列印任何元素,因為列表尚未計算!lazy list 被指定為延遲計算,而 lazy list 的 toString 方法小心不要強制進行任何額外的評估。

下面是一個更複雜的範例。它計算一個 lazy list,其中包含一個從給定的兩個數字開始的費氏數列。費氏數列是一個數列,其中每個元素都是數列中前兩個元素的總和。

scala> def fibFrom(a: Int, b: Int): LazyList[Int] = a #:: fibFrom(b, a + b)
fibFrom: (a: Int,b: Int)LazyList[Int]

此函數看似簡單。序列的第一個元素明顯是 a,而序列的其餘部分是從 b 開始的費氏數列,後接 a + b。棘手的部分是計算此序列而不造成無限遞迴。如果函數使用 :: 而不是 #::,則每次呼叫函數都會導致另一個呼叫,從而造成無限遞迴。不過,由於它使用 #::,因此在請求之前不會評估右側。以下是從兩個 1 開始的費氏數列的前幾個元素

scala> val fibs = fibFrom(1, 1).take(7)
fibs: scala.collection.immutable.LazyList[Int] = LazyList(<not computed>)
scala> fibs.toList
res9: List[Int] = List(1, 1, 2, 3, 5, 8, 13)

不可變 ArraySeq

當處理它們的演算法小心只處理其開頭時,清單非常有效率。存取、新增和移除清單的開頭僅需常數時間,而存取或修改清單中後面的元素則需要線性時間,深度進入清單。

ArraySeq 是一種集合類型(在 Scala 2.13 中引入),用於解決清單隨機存取的低效率問題。ArraySeq 允許以常數時間存取集合的任何元素。因此,使用 ArraySeq 的演算法不必小心只存取集合的開頭。它們可以存取任意位置的元素,因此寫起來可以更方便。

ArraySeq 的建立和更新方式與任何其他序列相同。

scala> val arr = scala.collection.immutable.ArraySeq(1, 2, 3)
arr: scala.collection.immutable.ArraySeq[Int] = ArraySeq(1, 2, 3)
scala> val arr2 = arr :+ 4
arr2: scala.collection.immutable.ArraySeq[Int] = ArraySeq(1, 2, 3, 4)
scala> arr2(0)
res22: Int = 1

ArraySeqs 不可變,因此您無法就地變更元素。然而,updatedappendedprepended 作業會建立新的 ArraySeq,這些 ArraySeq 僅在單一元素上與既有的 ArraySeq 不同

scala> arr.updated(2, 4)
res26: scala.collection.immutable.ArraySeq[Int] = ArraySeq(1, 2, 4)
scala> arr
res27: scala.collection.immutable.ArraySeq[Int] = ArraySeq(1, 2, 3)

如上方的最後一行所示,呼叫 updated 對原始 ArraySeq arr 沒有影響。

ArraySeqs 將其元素儲存在私有的 陣列 中。這是一種支援快速索引存取的精簡表示法,但更新或新增一個元素是線性的,因為它需要建立另一個陣列並複製所有原始陣列的元素。

向量

我們在先前的章節中看過,在某些特定使用案例中,ListArraySeq 是有效的資料結構,但它們在其他使用案例中也效率不彰:例如,對 List 而言,新增元素是常數,但對 ArraySeq 而言是線性的,反之,對 ArraySeq 而言,索引存取是常數,但對 List 而言是線性的。

Vector 是一種集合類型,對其所有作業提供良好的效能。向量允許在「有效地」常數時間內存取序列中的任何元素。這是一個比存取 List 的頭部或讀取 ArraySeq 的元素更大的常數,但它仍然是一個常數。因此,使用向量的演算法不必小心只存取序列的頭部。它們可以在任意位置存取和修改元素,因此它們的撰寫可能更為方便。

向量是建構和修改,就像任何其他序列一樣。

scala> val vec = scala.collection.immutable.Vector.empty
vec: scala.collection.immutable.Vector[Nothing] = Vector()
scala> val vec2 = vec :+ 1 :+ 2
vec2: scala.collection.immutable.Vector[Int] = Vector(1, 2)
scala> val vec3 = 100 +: vec2
vec3: scala.collection.immutable.Vector[Int] = Vector(100, 1, 2)
scala> vec3(0)
res1: Int = 100

向量表示為具有高分支因子的樹(樹或圖的分支因子是每個節點的子節點數)。有關如何完成此操作的詳細資訊 已變更 為 Scala 2.13.2,但基本概念保持不變,如下所示。

每個樹節點最多包含 32 個向量的元素,或最多包含 32 個其他樹節點。最多包含 32 個元素的向量可以在單一節點中表示。最多包含 32 * 32 = 1024 個元素的向量可以用單一間接表示。從樹根到最終元素節點的兩次跳躍足以應付最多包含 215 個元素的向量,三次跳躍足以應付 220 個元素的向量,四次跳躍足以應付 225 個元素的向量,五次跳躍足以應付最多 230 個元素的向量。因此,對於所有合理大小的向量,元素選取涉及最多 5 次基本陣列選取。當我們寫道元素存取是「有效地恆定時間」時,這就是我們的用意。

與選取類似,函式向量更新也是「有效地恆定時間」。更新向量中間的元素可以透過複製包含該元素的節點,以及指向該節點的每個節點(從樹根開始)來完成。這表示函式更新會建立一到五個節點,每個節點最多包含 32 個元素或子樹。這肯定比可變陣列中的就地更新昂貴,但仍然比複製整個向量便宜得多。

由於向量在快速隨機選取和快速隨機功能更新之間取得良好的平衡,因此它們目前是不可變索引序列的預設實作

scala> collection.immutable.IndexedSeq(1, 2, 3)
res2: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3)

不可變佇列

佇列是先進先出的序列。您可以使用 enqueue 將元素加入佇列,並使用 dequeue 從佇列中移除元素。這些操作都是常數時間。

以下是建立一個空的不可變佇列的方法

scala> val empty = scala.collection.immutable.Queue[Int]()
empty: scala.collection.immutable.Queue[Int] = Queue()

您可以使用 enqueue 將元素附加到不可變佇列

scala> val has1 = empty.enqueue(1)
has1: scala.collection.immutable.Queue[Int] = Queue(1)

若要將多個元素附加到佇列,請呼叫 enqueueAll,並將集合作為其參數

scala> val has123 = has1.enqueueAll(List(2, 3))
has123: scala.collection.immutable.Queue[Int]
    = Queue(1, 2, 3)

若要從佇列的頭部移除元素,請使用 dequeue

scala> val (element, has23) = has123.dequeue
element: Int = 1
has23: scala.collection.immutable.Queue[Int] = Queue(2, 3)

請注意,dequeue 會傳回一個配對,其中包含已移除的元素和佇列的其餘部分。

範圍

範圍是有序的整數序列,其間隔相等。例如,「1、2、3」是一個範圍,「5、8、11、14」也是一個範圍。若要在 Scala 中建立範圍,請使用預定義的方法 toby

scala> 1 to 3
res2: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3)
scala> 5 to 14 by 3
res3: scala.collection.immutable.Range = Range(5, 8, 11, 14)

如果您想要建立一個不包含其上限的範圍,請使用方便的方法 until,而不是 to

scala> 1 until 3
res2: scala.collection.immutable.Range = Range(1, 2)

範圍以常數空間表示,因為它們可以用三個數字定義:其開始、結束和遞增值。由於這個表示,範圍上的大多數操作都非常快速。

壓縮雜湊陣列對應前綴樹

雜湊嘗試是實作不可變集合和對應高效能的標準方式。壓縮雜湊陣列對應前綴樹是 JVM 上雜湊嘗試的一種設計,可改善區域性並確保樹狀結構維持在標準且緊湊的表示形式。它們由類別 immutable.HashMap 提供支援。它們的表示形式類似於向量,因為它們也是樹狀結構,其中每個節點有 32 個元素或 32 個子樹。但現在這些金鑰的選取是根據雜湊碼進行。例如,要在對應中尋找給定的金鑰,首先取得金鑰的雜湊碼。然後,使用雜湊碼的最低 5 個位元來選取第一個子樹,接著是下一個 5 個位元,依此類推。選取會在節點中儲存的所有元素在到目前為止選取的位元中具有不同的雜湊碼時停止。

雜湊嘗試在合理快速的查詢和合理有效率的功能插入 (+) 和刪除 (-) 之間取得良好的平衡。這就是它們成為 Scala 不可變對應和集合預設實作的原因。事實上,Scala 對包含少於五個元素的不可變集合和對應有進一步的最佳化。具有 1 到 4 個元素的集合和對應儲存為單一物件,其中僅包含元素(或在對應的情況下為金鑰/值對)作為欄位。空的不可變集合和空的不可變對應在每個情況下都是單一物件,不需要為它們重複儲存,因為空的不可變集合或對應將永遠保持為空。

紅黑樹

紅黑樹是一種平衡二元樹,其中一些節點指定為「紅色」,而其他節點指定為「黑色」。如同任何平衡二元樹,對它們執行的操作會在對數時間內可靠地完成,該時間與樹狀結構的大小成正比。

Scala 提供使用紅黑樹的不可變集合和映射的實作。在 TreeSetTreeMap 名稱下存取它們。

scala> scala.collection.immutable.TreeSet.empty[Int]
res11: scala.collection.immutable.TreeSet[Int] = TreeSet()
scala> res11 + 1 + 3 + 3
res12: scala.collection.immutable.TreeSet[Int] = TreeSet(1, 3)

紅黑樹是 Scala 中 SortedSet 的標準實作,因為它們提供一個有效率的迭代器,以排序順序傳回所有元素。

不可變位元組

BitSet 以較大整數的位元來表示小型整數的集合。例如,包含 3、2 和 0 的位元組將以二進位 1101 表示,也就是十進位的 13。

在內部,位元組使用 64 位元 Long 的陣列。陣列中的第一個 Long 是用於整數 0 到 63,第二個是 64 到 127,以此類推。因此,只要集合中最大的整數小於幾百個,位元組就會非常精簡。

位元組上的運算非常快速。包含測試需要常數時間。將項目新增到集合需要與位元組陣列中 Long 的數量成正比的時間,通常為小數字。以下是位元組使用的一些簡單範例

scala> val bits = scala.collection.immutable.BitSet.empty
bits: scala.collection.immutable.BitSet = BitSet()
scala> val moreBits = bits + 3 + 4 + 4
moreBits: scala.collection.immutable.BitSet = BitSet(3, 4)
scala> moreBits(3)
res26: Boolean = true
scala> moreBits(0)
res27: Boolean = false

向量映射

VectorMap 使用 Vector 的金鑰和 HashMap 來表示映射。它提供一個迭代器,以插入順序傳回所有條目。

scala> val vm = scala.collection.immutable.VectorMap.empty[Int, String]
vm: scala.collection.immutable.VectorMap[Int,String] =
  VectorMap()
scala> val vm1 = vm + (1 -> "one")
vm1: scala.collection.immutable.VectorMap[Int,String] =
  VectorMap(1 -> one)
scala> val vm2 = vm1 + (2 -> "two")
vm2: scala.collection.immutable.VectorMap[Int,String] =
  VectorMap(1 -> one, 2 -> two)
scala> vm2 == Map(2 -> "two", 1 -> "one")
res29: Boolean = true

第一行顯示 VectorMap 的內容會保留插入順序,最後一行顯示 VectorMap 可與其他 Map 比較,且此比較不會考慮元素的順序。

ListMap

ListMap 將地圖表示為鍵值對的連結清單。一般來說,對清單地圖進行的操作可能必須遍歷整個清單。因此,對清單地圖進行的操作會花費與地圖大小成線性的時間。事實上,Scala 中幾乎沒有使用清單地圖,因為標準不可變地圖幾乎總是比較快。唯一可能的例外是,如果地圖出於某種原因以清單中的第一個元素比其他元素更常被選取的方式建構。

scala> val map = scala.collection.immutable.ListMap(1->"one", 2->"two")
map: scala.collection.immutable.ListMap[Int,java.lang.String] =
    Map(1 -> one, 2 -> two)
scala> map(2)
res30: String = "two"

此頁面的貢獻者