IT老哥node
老哥是經過自學進入大廠作資深Java工程師,天天分享技術乾貨,助你進大廠算法
有不少東西以前在學的時候沒怎麼注意,筆者也是在重溫HashMap的時候發現有不少能夠去細究的問題,最終是會迴歸於數學的,如HashMap的加載因子爲何是0.75? 數組
本文主要對如下內容進行介紹:緩存
爲何HashMap須要加載因子?數據結構
解決衝突有什麼方法?less
爲何加載因子必定是0.75?而不是0.8,0.6?dom
HashMap的底層是哈希表,是存儲鍵值對的結構類型,它須要經過必定的計算才能夠肯定數據在哈希表中的存儲位置:函數
`static final int hash(Object key) {` `int h;` `return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);` `}` `// AbstractMap` `public int hashCode() {` `int h = 0;` `Iterator<Entry<K,V>> i = entrySet().iterator();` `while (i.hasNext())` `h += i.next().hashCode();` `return h;` `}`
通常的數據結構,不是查詢快就是插入快,HashMap就是一個插入慢、查詢快的數據結構。性能
但這種數據結構容易產生兩種問題:① 若是空間利用率高,那麼通過的哈希算法計算存儲位置的時候,會發現不少存儲位置已經有數據了(哈希衝突);② 若是爲了不發生哈希衝突,增大數組容量,就會致使空間利用率不高。spa
而加載因子就是表示Hash表中元素的填滿程度。
加載因子 = 填入表中的元素個數 / 散列表的長度
加載因子越大,填滿的元素越多,空間利用率越高,但發生衝突的機會變大了;
加載因子越小,填滿的元素越少,衝突發生的機會減少,但空間浪費了更多了,並且還會提升擴容rehash操做的次數。
衝突的機會越大,說明須要查找的數據還須要經過另外一個途徑查找,這樣查找的成本就越高。所以,必須在「衝突的機會」與「空間利用率」之間,尋找一種平衡與折衷。
因此咱們也能知道,影響查找效率的因素主要有這幾種:
散列函數是否能夠將哈希表中的數據均勻地散列?
怎麼處理衝突?
哈希表的加載因子怎麼選擇?
本文主要對後兩個問題進行介紹。
`Hi = (H(key) + di) MOD m,其中i=1,2,…,k(k<=m-1)`
H(key)爲哈希函數,m爲哈希表表長,di爲增量序列,i爲已發生衝突的次數。其中,開放定址法根據步長不一樣能夠分爲3種:
簡單地說,就是以當前衝突位置爲起點,步長爲1循環查找,直到找到一個空的位置,若是循環完了都佔不到位置,就說明容器已經滿了。舉個栗子,就像你在飯點去街上吃飯,挨家去看是否有位置同樣。
相對於線性探查法,這就至關於的步長爲di = i2來循環查找,直到找到空的位置。以上面那個例子來看,如今你不是挨家去看有沒有位置了,而是拿手機算去第i2家店,而後去問這家店有沒有位置。
這個就是取隨機數來做爲步長。仍是用上面的例子,此次就是徹底按心情去選一家店問有沒有位置了。
但開放定址法有這些缺點:
這種方法創建起來的哈希表,當衝突多的時候數據容易堆集在一塊兒,這時候對查找不友好;
刪除結點的時候不能簡單將結點的空間置空,不然將截斷在它填入散列表以後的同義詞結點查找路徑。所以若是要刪除結點,只能在被刪結點上添加刪除標記,而不能真正刪除結點;
若是哈希表的空間已經滿了,還須要創建一個溢出表,來存入多出來的元素。
`Hi = RHi(key), 其中i=1,2,…,k`
RHi()函數是不一樣於H()的哈希函數,用於同義詞發生地址衝突時,計算出另外一個哈希函數地址,直到不發生衝突位置。這種方法不容易產生堆集,可是會增長計算時間。
因此再哈希法的缺點是:增長了計算時間。
假設哈希函數的值域爲[0, m-1],設向量HashTable[0,…,m-1]爲基本表,每一個份量存放一個記錄,另外還設置了向量OverTable[0,…,v]爲溢出表。基本表中存儲的是關鍵字的記錄,一旦發生衝突,無論他們哈希函數獲得的哈希地址是什麼,都填入溢出表。
但這個方法的缺點在於:查找衝突數據的時候,須要遍歷溢出表才能獲得數據。
將衝突位置的元素構形成鏈表。在添加數據的時候,若是哈希地址與哈希表上的元素衝突,就放在這個位置的鏈表上。
拉鍊法的優勢:
處理衝突的方式簡單,且無堆集現象,非同義詞毫不會發生衝突,所以平均查找長度較短;
因爲拉鍊法中各鏈表上的結點空間是動態申請的,因此它更適合造表前沒法肯定表長的狀況;
刪除結點操做易於實現,只要簡單地刪除鏈表上的相應的結點便可。
拉鍊法的缺點:須要額外的存儲空間。
從HashMap的底層結構中咱們能夠看到,HashMap採用是數組+鏈表/紅黑樹的組合來做爲底層結構,也就是開放地址法+鏈地址法的方式來實現HashMap。
從上文咱們知道,HashMap的底層其實也是哈希表(散列表),而解決衝突的方式是鏈地址法。HashMap的初始容量大小默認是16,爲了減小衝突發生的機率,當HashMap的數組長度到達一個臨界值的時候,就會觸發擴容,把全部元素rehash以後再放在擴容後的容器中,這是一個至關耗時的操做。
而這個臨界值就是由加載因子和當前容器的容量大小來肯定的:
臨界值 = DEFAULT\_INITIAL\_CAPACITY * DEFAULT\_LOAD\_FACTOR
即默認狀況下是16x0.75=12時,就會觸發擴容操做。
那麼爲何選擇了0.75做爲HashMap的加載因子呢?這個跟一個統計學裏很重要的原理——泊松分佈有關。
泊松分佈是統計學和機率學常見的離散機率分佈,適用於描述單位時間內隨機事件發生的次數的機率分佈。有興趣的讀者能夠看看維基百科或者阮一峯老師的這篇文章:泊松分佈和指數分佈:10分鐘教程[1]
等號的左邊,P 表示機率,N表示某種函數關係,t 表示時間,n 表示數量。等號的右邊,λ 表示事件的頻率。
在HashMap的源碼中有這麼一段註釋:
`* Ideally, under random hashCodes, the frequency of` `* nodes in bins follows a Poisson distribution` `* (http://en.wikipedia.org/wiki/Poisson_distribution) with a` `* parameter of about 0.5 on average for the default resizing` `* threshold of 0.75, although with a large variance because of` `* resizing granularity. Ignoring variance, the expected` `* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /` `* factorial(k)). The first values are:` `* 0: 0.60653066` `* 1: 0.30326533` `* 2: 0.07581633` `* 3: 0.01263606` `* 4: 0.00157952` `* 5: 0.00015795` `* 6: 0.00001316` `* 7: 0.00000094` `* 8: 0.00000006` `* more: less than 1 in ten million`
在理想狀況下,使用隨機哈希碼,在擴容閾值(加載因子)爲0.75的狀況下,節點出如今頻率在Hash桶(表)中遵循參數平均爲0.5的泊松分佈。忽略方差,即X = λt,P(λt = k),其中λt = 0.5的狀況,按公式:
計算結果如上述的列表所示,當一個bin中的鏈表長度達到8個元素的時候,機率爲0.00000006,幾乎是一個不可能事件。
因此咱們能夠知道,其實常數0.5是做爲參數代入泊松分佈來計算的,而加載因子0.75是做爲一個條件,當HashMap長度爲length/size ≥ 0.75時就擴容,在這個條件下,衝突後的拉鍊長度和機率結果爲:
`0: 0.60653066` `1: 0.30326533` `2: 0.07581633` `3: 0.01263606` `4: 0.00157952` `5: 0.00015795` `6: 0.00001316` `7: 0.00000094` `8: 0.00000006`
HashMap中除了哈希算法以外,有兩個參數影響了性能:初始容量和加載因子。初始容量是哈希表在建立時的容量,加載因子是哈希表在其容量自動擴容以前能夠達到多滿的一種度量。
在維基百科來描述加載因子:
對於開放定址法,加載因子是特別重要因素,應嚴格限制在0.7-0.8如下。超過0.8,查表時的CPU緩存不命中(cache missing)按照指數曲線上升。所以,一些採用開放定址法的hash庫,如Java的系統庫限制了加載因子爲0.75,超過此值將resize散列表。
在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減小擴容rehash操做次數,因此,通常在使用HashMap時建議根據預估值設置初始容量,以便減小擴容操做。
選擇0.75做爲默認的加載因子,徹底是時間和空間成本上尋求的一種折衷選擇。