[轉]爲何Java中的HashMap默認加載因子是0.75

前幾天在一個羣裏看到有人討論hashmap中的加載因子爲何是默認0.75。html

HashMap源碼中的加載因子java

static final float DEFAULT_LOAD_FACTOR = 0.75f; 

當時想到的是應該是「哈希衝突」和「空間利用率」矛盾的一個折衷。
跟數據結構要麼查詢快要麼插入快一個道理,hashmap就是一個插入慢、查詢快的數據結構。node

加載因子是表示Hash表中元素的填滿的程度。
加載因子越大,填滿的元素越多,空間利用率越高,但衝突的機會加大了。
反之,加載因子越小,填滿的元素越少,衝突的機會減少,但空間浪費多了。數組

衝突的機會越大,則查找的成本越高。反之,查找的成本越小。網絡

所以,必須在 "衝突的機會"與"空間利用率"之間尋找一種平衡與折衷。數據結構

哈希衝突主要與兩個因素有關,(1)填裝因子,填裝因子是指哈希表中已存入的數據元素個數與哈希地址空間的大小的比值,a=n/m ; a越小,衝突的可能性就越小,相反則衝突可能性較大;可是a越小空間利用率也就越小,a越大,空間利用率越高,爲了兼顧哈希衝突和存儲空間利用率,一般將a控制在0.6-0.9之間,而.net中的HashTable則直接將a的最大值定義爲0.72 (雖然微軟官方MSDN中聲明HashTable默認填裝因子爲1.0,但實際上都是0.72的倍數),(2)與所用的哈希函數有關,若是哈希函數得當,就可使哈希地址儘量的均勻分佈在哈希地址空間上,從而減小衝突的產生,但一個良好的哈希函數的得來很大程度上取決於大量的實踐,不過幸虧前人已經總結實踐了不少高效的哈希函數,能夠參考大神Lucifer文章:數據結構:HashTable: http://www.cnblogs.com/lucifer1982/archive/2008/06/18/1224319.htmlless


可是爲何必定是0.75?而不是0.8,0.6

本着不嫌事大的精神繼續深挖,在此以前先簡單補充點本文須要的基礎知識:dom

1.衝突定義:假設哈希表的地址集爲[0,n),衝突是指由關鍵字獲得的哈希地址爲j(0<=j<=n-1)的位置上已經有記錄。在關鍵字獲得的哈希地址上已經有記錄,那麼就稱之爲衝突。函數

2.處理衝突:就是爲該關鍵字的記錄扎到另外一個「空」的哈希地址。即在處理哈希地址的衝突時,若獲得的另外一個哈希地址H1仍然發生衝突,則再求下一個地址H2,若H2仍然衝突,再求的H3,直至Hk不發生衝突爲止,則Hk爲記錄在表中的地址。spa


處理衝突的幾種方法:

1、 開放定址法

Hi=(H(key) + di) MOD m i=1,2,...k(k<=m-1)其中H(key)爲哈希函數;m爲哈希表表長;di爲增量序列。

開放定址法根據步長不一樣能夠分爲3種:

1)線性探查法(Linear Probing):di=1,2,3,...,m-1
  簡單地說就是以當前衝突位置爲起點,步長爲1循環查找,直到找到一個空的位置就把元素插進去,循環完了都找不到說明容器滿了。就像你去一條街上的店裏吃飯,問了第一家被告知滿座,而後挨着一家家去問是否有位置同樣。

2)線性補償探測法:di=Q 下一個位置知足 Hi=(H(key) + Q) mod m i=1,2,...k(k<=m-1) ,要求 Q 與 m 是互質的,以便能探測到哈希表中的全部單元。
繼續用上面的例子,如今你不是挨着一家家去問了,拿出計算器算了一下,而後隔Q家問一次有沒有位置。

3)僞隨機探測再散列:di=僞隨機數序列。仍是那個例子,這是徹底根據心情去選一家店來問了

缺點:

  • 這種方法創建起來的hash表當衝突多的時候數據容易堆聚在一塊兒,這時候對查找不友好;
  • 刪除結點不能簡單地將被刪結 點的空間置爲空,不然將截斷在它以後填人散列表的同義詞結點的查找路徑。所以在 用開放地址法處理衝突的散列表上執行刪除操做,只能在被刪結點上作刪除標記,而不能真正刪除結點
  • 當空間滿了,還要創建一個溢出表來存多出來的元素。

2、再哈希法

Hi = RHi(key),i=1,2,...k
RHi均是不一樣的哈希函數,即在同義詞產生地址衝突時計算另外一個哈希函數地址,直到不發生衝突爲止。這種方法不易產生彙集,可是增長了計算時間。

缺點:增長了計算時間。

3、創建一個公共溢出區

假設哈希函數的值域爲[0,m-1],則設向量HashTable[0...m-1]爲基本表,每一個份量存放一個記錄,另設立向量OverTable[0....v]爲溢出表。全部關鍵字和基本表中關鍵字爲同義詞的記錄,無論他們由哈希函數獲得的哈希地址是什麼,一旦發生衝突,都填入溢出表。

簡單地說就是搞個新表存衝突的元素。

4、鏈地址法(拉鍊法)

將全部關鍵字爲同義詞的記錄存儲在同一線性鏈表中,也就是把衝突位置的元素構形成鏈表。

拉鍊法的優勢:

  • 拉鍊法處理衝突簡單,且無堆積現象,即非同義詞決不會發生衝突,所以平均查找長度較短;
  • 因爲拉鍊法中各鏈表上的結點空間是動態申請的,故它更適合於造表前沒法肯定表長的狀況;
  • 在用拉鍊法構造的散列表中,刪除結點的操做易於實現。只要簡單地刪去鏈表上相應的結點便可。

拉鍊法的缺點:

  • 指針須要額外的空間,故當結點規模較小時,開放定址法較爲節省空間,而若將節省的指針空間用來擴大散列表的規模,可以使裝填因子變小,這又減小了開放定址法中的衝突,從而提升平均查找速度

Java中HashMap的數據結構

HashMap其實是一個「鏈表散列」的數據結構,即數組和鏈表的結合體。

 
HashMap數據結構,來源於網絡

看圖就能夠知道Java中的hashMap使用了拉鍊法處理衝突。
HashMap有一個初始容量大小,默認是16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 

爲了減小衝突的機率,當hashMap的數組長度到了一個臨界值就會觸發擴容,把全部元素rehash再放到擴容後的容器中,這是一個很是耗時的操做。

而這個臨界值由【加載因子】和當前容器的容量大小來肯定:DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR ,即默認狀況下是16x0.75=12時,就會觸發擴容操做。

因此使用hash容器時儘可能預估本身的數據量來設置初始值。具體代碼實現自行去研究HashMap的源碼。

基礎知識補充完畢,回到正題,爲何加載因子要默認是0.75?
從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

注意wiki連接中的關鍵字:Poisson_distribution
泊淞分佈啊

簡單翻譯一下就是在理想狀況下,使用隨機哈希碼,節點出現的頻率在hash桶中遵循泊松分佈,同時給出了桶中元素個數和機率的對照表。

從上面的表中能夠看到當桶中元素到達8個的時候,機率已經變得很是小,也就是說用0.75做爲加載因子,每一個碰撞位置的鏈表長度超過8個是幾乎不可能的。

好了,再深挖就要挖到統計學那邊去了,就此打住,重申一下使用hash容器請儘可能指定初始容量,且是2的冪次方。

關於泊淞分佈的知識請看

http://www.ruanyifeng.com/blog/2015/06/poisson-distribution.html#comment-356111



做者:Eric新之助
連接: https://www.jianshu.com/p/dff8f4641814 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索