Scala 中的集合(二):集合性能比較

本文由 Shaw 發表在 ScalaCool 團隊博客。性能

在平時使用集合的時候,咱們常常會選擇 Scala 中通用的集合,例如:SeqMapList 等等,有的時候選擇「通用集合」徹底能夠解決問題,可是當集合操做變得很複雜以致於涉及到「性能問題」的時候,採用「通用集合」可能並非一個好的選擇。在不一樣的場景下選擇合適的集合可使咱們對於集合的操做更加高效。spa

大部分狀況下,咱們都會優先採用「不可變集合」,因此本文將經過比較幾種常見的「不可變集合」來闡述各個集合之間的性能差別。scala

Set

Image of Set

經過上圖能夠看到,兩種經常使用的類型:TreeSetHashSet 都繼承至 Set3d

TreeSet

TreeSet 是用「紅黑樹」來實現的,「紅黑樹」是一種相對平衡的二叉查找樹,它能夠在 O(log2 n) 時間複雜度內作查找,例如:code

val s = scala.collection.immutable.TreeSet(1, 2, 4, 5, 7, 8, 11, 14, 15)
s: scala.collection.immutable.TreeSet[Int] = TreeSet(1, 2, 4, 5, 7, 8, 11, 14, 15)複製代碼

則其對應的紅黑樹爲:cdn

image of redBlackTree

從上面「紅黑樹」的結構能夠看到在對 TreeSet 進行查找或者修改操做時,其時間複雜度爲 O(log2 n)blog

HashSet

HashSet 是用 Hash Trie 來實現的,從表現形式上能夠將 HashSet 看做是一種樹結構,該樹的每一個節點包含32個元素或者32個子樹,每一個節點都存儲相應的 hashcode ,爲了方便描述這種結構咱們先定義一個 HashSet 的實例,並將該實例用圖表現出來:繼承

scala> val s = scala.collection.immutable.HashSet(1, 3, 33, 35, 50, 289, 306, 1057)
s: scala.collection.immutable.HashSet[Int] = Set(289, 1, 1057, 33, 306, 3, 35, 50)複製代碼

看到上面的代碼,咱們或許會有一個疑問,就是獲得的 HashSet 中各個元素的順序好像變了,這是由於在實現 HashSet 時,元素的順序不是按照咱們給定的順序來的,而是根據元素對應的 hashcode 來決定的,在 HashSet 中,元素的 hashcode是經過下面的操做獲得的:開發

def getHashCode(key: Int) = {
    val hcode = key.##
    var h: Int = hcode + ~(hcode << 9)
    h = h ^ (h >>> 14)
    h = h + (h << 4)
    h ^ (h >>> 10)
}複製代碼

爲了方便理解,咱們這裏規定元素的 hashcode 就是它自己,那麼以前的代碼就變成了:get

scala> val s = scala.collection.immutable.HashSet(1, 3, 33, 35, 50, 289, 306, 1057)
s: scala.collection.immutable.HashSet[Int] = Set(1, 33, 1057, 289, 3, 35, 50, 306)複製代碼

其對應的樹結構爲:

image of hashSet

經過上圖,咱們能夠看到「樹」的每一個節點都存儲相應的 hashcode,在這棵「樹」上查找某個值時,首先用該元素對應的 hashcode 的最後 5bit 查找第一層「子樹」,而後毎 5bit 找到下一層 「子樹」。當存儲在一個節點中全部元素的表明他們當前所在層的 hashcode 位都不相同時,查找結束。例如:

若是咱們要查找數字 1057 是否在這棵「樹」上面:

  1. 1057 轉換爲 「二進制」,咱們獲得 00001 00001 00001,而後取出最後的 5bit00001

  2. 查找第一層「子樹」,找到 00001 對應的節點,該節點有三個「孩子」,因此咱們要進入下一層,接下來取出第二個「五位」:00001

  3. 查找第二層「子樹」,找到 00001 對應的節點,該節點有兩個「孩子」,因此咱們要進入下一層,接下來取出第三個「五位」:00001

  4. 查找第三層「子樹」,找到 00001 對應的節點,該節點就只有一個元素 1057,因此咱們找到了。

在這棵樹中,咱們查詢 1057 的時間複雜度爲 O(3),因爲 Hashset 中的每個節點均可以有 32 個分支,因此其在查詢或者修改等操做時的效率會大大提升,例如:對於一個擁有100萬個元素的 HashSet,咱們只須要四層節點。(由於106 ≈ 324),咱們在查詢其中的某一個元素時,最多隻須要 O(4) 的時間複雜度,而採用 TreeSet 就須要 O(20) 的時間複雜度,因此在不出現「哈希碰撞」的狀況下(在平常開發中使用 HashSet 極少會出現「哈希碰撞」),HashSet 的隨機訪問時間複雜度爲 log32 n,比前面介紹的 TreeSet 要好。

總結

經過前面咱們對兩種 Set 的比較,咱們能夠得出:

  1. 當集合中元素不是不少,並且對效率要求不高的時候,選擇通用的 Set 就能夠解決問題;

  2. 當元素數量很是龐大,而且對效率要求比較高的時候,咱們通常選擇 HashSet

  3. 當選擇 HashSet 時,出現很嚴重的 「哈希碰撞」時,採用 TreeSet

Map

Image of Map

如上圖所示,Map 支持三種類型:HashMapTreeMapListMap,其中比較經常使用的是前面兩種。

HashMap、TreeMap

HashMap 與咱們前面提到的 HashSet 結構相似,一樣,TreeMapTreeSet 結構相似,通常狀況下,優先選擇 HashMap

ListMap

ListMap 是一種「鏈表」結構,在對其中的元素進行操做的時候,咱們一般都會去遍歷其中的元素,因此其查詢、修改等操做的時間複雜度也同列表長度成「線性關係」,通常狀況下,在 Scala 中,咱們不多使用 ListMap,只有當 Map 中處在前面的元素的訪問頻率遠遠大於處在後面的元素時,纔會採用 ListMap

總結

  1. 當集合中元素不是不少,並且對效率要求不高的時候,選擇通用的 Map 就能夠解決問題

  2. 當元素數量很是龐大,而且對效率要求比較高的時候,咱們通常選擇 HashMap

  3. 當選擇 HashSet 時,出現很嚴重的 「哈希碰撞」時,採用 TreeMap

  4. Map 中處在前面的元素的訪問頻率遠遠大於處在後面的元素時,採用 ListMap

Seq

Image of Seq

經過上圖能夠看到,兩種經常使用的類型:VectorList 都繼承至 Seq

Vector

Vector 的結構與咱們前面提到的 HashSet 很是的相似,咱們能夠將 Vector 當作是由元素的「下標」組成的「前綴樹」,該樹的每一個節點也包含32個元素或者32個子樹,每一個節點存儲相應下標對應的元素以及具備相同「前綴」的「孩子」,爲了方便描述,咱們依然先定義一個 Vector 的實例:

scala> val v = (0 to 1057).toVector
v: Vector[Int] = Vector(0, 1, 2, 3, ... , 1057)複製代碼

咱們定義了一個具備 1058 個元素的 Vector,每個元素的下標與該元素的值相等。接下來咱們用圖將該實例表現出來:

Image of vector trie

上圖展現了實例中的部分元素,能夠看到具備相同「前綴」的元素擁有相同的「父親」,例如:

元素 333550對應的「二進制」分別是:00001 0000100001 0001100001 10010,它們的「高五位」也就是「前綴」都是 00001

如今咱們查找其中的某個元素 1057

  1. 1057 對應的下標是 1057,轉換爲二進制爲:00001 00001 00001

  2. 1057 最高五位也就是第一個前綴爲 00001,在第一層「子樹」中找到 00001 對應的節點;

  3. 第二個五位也就是第二個 「前綴」是 00001,則在第二層「子樹」中找到 00001 對應的節點;

  4. 最後一個五位是 00001,在第三層子樹中找到 00001 對應的節點,則該元素存在於這個節點中。

能夠看到咱們查詢 1057 的時間複雜度爲:O(3),因爲 Vector 也是採用具備 32 分支的樹結構,因此它的查詢、修改等操做的時間複雜度也是 log32 n,因爲下標不會重複,因此不會像 HashSet 那樣出現 「哈希碰撞」,因此它的效率比 HashSet 要好。

Scala 中使用集合的時候,若是沒有特別的要求,咱們應該首先選擇 Vector。固然,vector 也有不適用的場景,若是咱們要頻繁地執行一個集合的「頭」和「尾」的操做,選擇 Vector 就不太好了,這時咱們能夠選擇 List

List

在平常開發中咱們使用 List 的頻率很是高,List 是個 「單鏈表」結構,其中的每一個節點均可以看做是一個「格子」,每個「格子」持有兩個引用,一個引用指向值,另外一個引用指向後續的元素。

scala> val l = List(1, 2, 3)
l: List[Int] = List(1, 2, 3)複製代碼

其結構用圖表示出來爲:

Image of list

List 只有在操做 「頭部」和「尾部」時具備 O(1) 的複雜度,若是列表中的元素很是多,那 List 的效率遠遠不如前面提到的 Vector,因此只有當咱們須要頻繁操做集合中的首尾元素時,纔去選擇 List,大部分狀況下, Vector 應該是咱們缺省的選擇。

總結

  1. 通常狀況下,優先採用 Vector

  2. 只有在頭尾操做很是頻繁的時候選擇 List

相關文章
相關標籤/搜索