本文由 Shaw 發表在 ScalaCool 團隊博客。性能
在平時使用集合的時候,咱們常常會選擇 Scala 中通用的集合,例如:Seq
、Map
、List
等等,有的時候選擇「通用集合」徹底能夠解決問題,可是當集合操做變得很複雜以致於涉及到「性能問題」的時候,採用「通用集合」可能並非一個好的選擇。在不一樣的場景下選擇合適的集合可使咱們對於集合的操做更加高效。spa
大部分狀況下,咱們都會優先採用「不可變集合」,因此本文將經過比較幾種常見的「不可變集合」來闡述各個集合之間的性能差別。scala
經過上圖能夠看到,兩種經常使用的類型:TreeSet
、HashSet
都繼承至 Set
。3d
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
從上面「紅黑樹」的結構能夠看到在對 TreeSet
進行查找或者修改操做時,其時間複雜度爲 O(log2 n)
。blog
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)複製代碼
其對應的樹結構爲:
經過上圖,咱們能夠看到「樹」的每一個節點都存儲相應的 hashcode
,在這棵「樹」上查找某個值時,首先用該元素對應的 hashcode
的最後 5
個 bit
查找第一層「子樹」,而後毎 5
個 bit
找到下一層 「子樹」。當存儲在一個節點中全部元素的表明他們當前所在層的 hashcode
位都不相同時,查找結束。例如:
若是咱們要查找數字 1057
是否在這棵「樹」上面:
將 1057
轉換爲 「二進制」,咱們獲得 00001 00001 00001
,而後取出最後的 5
個 bit
:00001
;
查找第一層「子樹」,找到 00001
對應的節點,該節點有三個「孩子」,因此咱們要進入下一層,接下來取出第二個「五位」:00001
;
查找第二層「子樹」,找到 00001
對應的節點,該節點有兩個「孩子」,因此咱們要進入下一層,接下來取出第三個「五位」:00001
;
查找第三層「子樹」,找到 00001
對應的節點,該節點就只有一個元素 1057
,因此咱們找到了。
在這棵樹中,咱們查詢 1057
的時間複雜度爲 O(3)
,因爲 Hashset
中的每個節點均可以有 32
個分支,因此其在查詢或者修改等操做時的效率會大大提升,例如:對於一個擁有100萬個元素的 HashSet
,咱們只須要四層節點。(由於106 ≈ 324),咱們在查詢其中的某一個元素時,最多隻須要 O(4)
的時間複雜度,而採用 TreeSet
就須要 O(20)
的時間複雜度,因此在不出現「哈希碰撞」的狀況下(在平常開發中使用 HashSet
極少會出現「哈希碰撞」),HashSet
的隨機訪問時間複雜度爲 log32 n
,比前面介紹的 TreeSet
要好。
經過前面咱們對兩種 Set
的比較,咱們能夠得出:
當集合中元素不是不少,並且對效率要求不高的時候,選擇通用的 Set
就能夠解決問題;
當元素數量很是龐大,而且對效率要求比較高的時候,咱們通常選擇 HashSet
;
當選擇 HashSet
時,出現很嚴重的 「哈希碰撞」時,採用 TreeSet
。
如上圖所示,Map
支持三種類型:HashMap
、TreeMap
和 ListMap
,其中比較經常使用的是前面兩種。
HashMap
與咱們前面提到的 HashSet
結構相似,一樣,TreeMap
與 TreeSet
結構相似,通常狀況下,優先選擇 HashMap
。
ListMap
是一種「鏈表」結構,在對其中的元素進行操做的時候,咱們一般都會去遍歷其中的元素,因此其查詢、修改等操做的時間複雜度也同列表長度成「線性關係」,通常狀況下,在 Scala
中,咱們不多使用 ListMap
,只有當 Map
中處在前面的元素的訪問頻率遠遠大於處在後面的元素時,纔會採用 ListMap
。
當集合中元素不是不少,並且對效率要求不高的時候,選擇通用的 Map
就能夠解決問題
當元素數量很是龐大,而且對效率要求比較高的時候,咱們通常選擇 HashMap
;
當選擇 HashSet
時,出現很嚴重的 「哈希碰撞」時,採用 TreeMap
;
當 Map
中處在前面的元素的訪問頻率遠遠大於處在後面的元素時,採用 ListMap
。
經過上圖能夠看到,兩種經常使用的類型:Vector
、List
都繼承至 Seq
Vector
的結構與咱們前面提到的 HashSet
很是的相似,咱們能夠將 Vector
當作是由元素的「下標」組成的「前綴樹」,該樹的每一個節點也包含32個元素或者32個子樹,每一個節點存儲相應下標對應的元素以及具備相同「前綴」的「孩子」,爲了方便描述,咱們依然先定義一個 Vector
的實例:
scala> val v = (0 to 1057).toVector
v: Vector[Int] = Vector(0, 1, 2, 3, ... , 1057)複製代碼
咱們定義了一個具備 1058
個元素的 Vector
,每個元素的下標與該元素的值相等。接下來咱們用圖將該實例表現出來:
上圖展現了實例中的部分元素,能夠看到具備相同「前綴」的元素擁有相同的「父親」,例如:
元素 33
、35
、50
對應的「二進制」分別是:00001 00001
、00001 00011
、00001 10010
,它們的「高五位」也就是「前綴」都是 00001
。
如今咱們查找其中的某個元素 1057
:
1057
對應的下標是 1057
,轉換爲二進制爲:00001 00001 00001
;
1057
最高五位也就是第一個前綴爲 00001
,在第一層「子樹」中找到 00001
對應的節點;
第二個五位也就是第二個 「前綴」是 00001
,則在第二層「子樹」中找到 00001
對應的節點;
最後一個五位是 00001
,在第三層子樹中找到 00001
對應的節點,則該元素存在於這個節點中。
能夠看到咱們查詢 1057
的時間複雜度爲:O(3)
,因爲 Vector
也是採用具備 32
分支的樹結構,因此它的查詢、修改等操做的時間複雜度也是 log32 n
,因爲下標不會重複,因此不會像 HashSet
那樣出現 「哈希碰撞」,因此它的效率比 HashSet
要好。
在 Scala
中使用集合的時候,若是沒有特別的要求,咱們應該首先選擇 Vector
。固然,vector
也有不適用的場景,若是咱們要頻繁地執行一個集合的「頭」和「尾」的操做,選擇 Vector
就不太好了,這時咱們能夠選擇 List
。
在平常開發中咱們使用 List
的頻率很是高,List
是個 「單鏈表」結構,其中的每一個節點均可以看做是一個「格子」,每個「格子」持有兩個引用,一個引用指向值,另外一個引用指向後續的元素。
scala> val l = List(1, 2, 3)
l: List[Int] = List(1, 2, 3)複製代碼
其結構用圖表示出來爲:
List
只有在操做 「頭部」和「尾部」時具備 O(1)
的複雜度,若是列表中的元素很是多,那 List
的效率遠遠不如前面提到的 Vector
,因此只有當咱們須要頻繁操做集合中的首尾元素時,纔去選擇 List
,大部分狀況下, Vector
應該是咱們缺省的選擇。
通常狀況下,優先採用 Vector
;
只有在頭尾操做很是頻繁的時候選擇 List
。