裝載因子越大,說明散列表中的元素越多,空閒位置越少,散列衝突的機率就越大。java
不只插入數據的過程要屢次尋址或者拉很長的鏈,查找的過程也會所以變得很慢。api
對於沒有頻繁插入和刪除的靜態數據集合來講,很容易根據數據的特色、分佈等,設計出完美的、極少衝突的散列函數,由於畢竟以前數據都是已知的。數組
對於動態散列表來講,數據集合是頻繁變更的,事先沒法預估將要加入的數據個數,因此也沒法事先申請一個足夠大的散列表。緩存
隨着數據慢慢加入,裝載因子就會慢慢變大。當裝載因子大到必定程度以後,散列衝突就會變得不可接受。數據結構
這個時候,進行動態擴容,從新申請一個更大的散列表,將數據搬移到這個新散列表中。函數
假設每次擴容咱們都申請一個原來散列表大小兩倍的空間。性能
若是原來散列表的裝載因子是 0.8,那通過擴容以後,新散列表的裝載因子就降低爲原來的一半,變成了0.4。大數據
針對數組的擴容,數據搬移操做比較簡單。可是,針對散列表的擴容,數據搬移操做要複雜不少。由於散列表的大小變了,數據的存儲位置也變了,因此須要經過散列函數從新計算每一個數據的存儲位置。優化
以下圖所示在原來的散列表中,21這個元素原來存儲在下標爲0的位置,搬移到新的散列表中,存儲在下標爲7的位置。this
對於支持動態擴容的散列表,插入一個數據,最好狀況下,不須要擴容,最好時間複雜度是 O(1)。
最壞狀況下,散列表裝載因子太高,啓動擴容,須要從新申請內存空間,從新計算哈希位置,而且搬移數據,因此時間複雜度是O(n)。
均攤狀況下,時間複雜度接近最好狀況,就是O(1)。
實際上,對於動態散列表,隨着數據的刪除,散列表中的數據會愈來愈少,空閒空間會愈來愈多。
若是對空間消耗很是敏感,能夠在裝載因子小於某個值以後,啓動動態縮容。
固然,若是更加在乎執行效率,可以容忍多消耗一點內存空間,那就能夠不用費勁來縮容了。
當散列表的裝載因子超過某個閾值時,就須要進行擴容。因此裝載因子閾值須要選擇得當。若是太大,會致使衝突過多;若是過小,會致使內存浪費嚴重。
裝載因子閾值的設置要權衡時間、空間複雜度。
若是內存空間不緊張,對執行效率要求很高,能夠下降負載因子的閾值;相反,若是內存空間緊張,對執行效率要求又不高,能夠增長負載因子的值,甚至能夠大於1。
大部分狀況下,動態擴容的散列表插入一個數據都很快,可是在特殊狀況下,當裝載因子已經到達閾值,須要先進行擴容,再插入數據。
這個時候,插入數據就會變得很慢,甚至會沒法接受。
舉一個極端的例子,若是散列表當前大小爲1GB,要想擴容爲原來的兩倍大小,那就須要對1GB的數據從新計算哈希值,而且從原來的散列表搬移到新的散列表,這個操做很耗時。
若是業務代碼直接服務於用戶,儘管大部分狀況下,插入一個數據的操做都很快,可是,極個別很是慢的插入操做,也會讓用戶崩潰。這個時候,「一次性」擴容的機制就不合適了。
爲了解決一次性擴容耗時過多的狀況,能夠將擴容操做穿插在插入操做的過程當中,分批完成。
當裝載因子觸達閾值以後,只申請新空間,但並不將老的數據搬移到新散列表中。
當有新數據要插入時,將新數據插入新散列表中,而且從老的散列表中拿出一個數據放入到新散列表。每次插入一個數據到散列表,都重複上面的過程。
通過屢次插入操做以後,老的散列表中的數據就一點一點所有搬移到新散列表中了。這樣沒有了集中的一次性數據搬移,插入操做就都變得很快了。
這期間對於查詢操做,爲了兼容了新、老散列表中的數據,先重新散列表中查找,若是沒有找到,再去老的散列表中查找。
經過這樣均攤的方法,將一次性擴容的代價,均攤到屢次插入操做中,就避免了一次性擴容耗時過多的狀況。
這種實現方式,任何狀況下,插入一個數據的時間複雜度都是O(1)。
兩種主要的散列衝突的解決辦法,開放尋址法和鏈表法。
這兩種衝突解決辦法在實際的軟件開發中都很是經常使用。好比,Java中LinkedHashMap 就採用了鏈表法解決衝突,ThreadLocalMap 是經過線性探測的開放尋址法來解決衝突。
優勢:
缺點:
總結一下,當數據量比較小、裝載因子小的時候,適合採用開放尋址法。這也是 Java 中的 ThreadLocalMap 使用開放尋址法解決散列衝突的緣由。
總結一下,基於鏈表的散列衝突處理方法比較適合存儲大對象、大數據量的散列表,並且,比起開放尋址法,它更加靈活,支持更多的優化策略,好比用紅黑樹代替鏈表。
Java 中的 HashMap 是一個常常用到的散列表,來具體看下,這些技術是怎麼應用的。
int hash(Object key) { int h = key.hashCode(); return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小 } // 其中, hashCode() 返回的是 Java 對象的 hash code。好比 String 類型的對象的 hashCode() 就是下面這樣: public int hashCode() { int var1 = this.hash; if(var1 == 0 && this.value.length > 0) { char[] var2 = this.value; for(int var3 = 0; var3 < this.value.length; ++var3) { var1 = 31 * var1 + var2[var3]; } this.hash = var1; } return var1; }