1. 程式人生 > 實用技巧 >HashMap預設載入因子為什麼選擇0.75

HashMap預設載入因子為什麼選擇0.75

Hashtable 初始容量是11 ,擴容 方式為2N+1;

HashMap 初始容量是16,擴容方式為2N;  

阿里的人突然問我為啥擴容因子是0.75,回來總結了一下;提高空間利用率和 減少查詢成本的折中,主要是泊松分佈,0.75的話碰撞最小,

HashMap有兩個引數影響其效能:初始容量和載入因子。容量是雜湊表中桶的數量,初始容量只是雜湊表在建立時的容量。載入因子是雜湊表在其容量自動擴容之前可以達到多滿的一種度量。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,則要對該雜湊表進行擴容、rehash操作(即重建內部資料結構),擴容後的雜湊表將具有兩倍的原容量。

通常,載入因子需要在時間和空間成本上尋求一種折衷。

載入因子過高,例如為1,雖然減少了空間開銷,提高了空間利用率,但同時也增加了查詢時間成本;

載入因子過低,例如0.5,雖然可以減少查詢時間成本,但是空間利用率很低,同時提高了rehash操作的次數

在設定初始容量時應該考慮到對映中所需的條目數及其載入因子,以便最大限度地減少rehash操作次數,所以,一般在使用HashMap時建議根據預估值設定初始容量,減少擴容操作。

選擇0.75作為預設的載入因子,完全是時間和空間成本上尋求的一種折衷選擇,

正文

前幾天在一個群裡看到有人討論hashmap中的載入因子為什麼是預設0.75。

HashMap原始碼中的載入因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;  

當時想到的是應該是“雜湊衝突”和“空間利用率”矛盾的一個折衷。

跟資料結構要麼查詢快要麼插入快一個道理,hashmap就是一個插入慢、查詢快的資料結構。

載入因子是表示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.html


但是為什麼一定是0.75?而不是0.8,0.6#

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

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

2.處理衝突:就是為該關鍵字的記錄扎到另一個“空”的雜湊地址。即在處理雜湊地址的衝突時,若得到的另一個雜湊地址H1仍然發生衝突,則再求下一個地址H2,若H2仍然衝突,再求的H3,直至Hk不發生衝突為止,則Hk為記錄在表中的地址。


回到頂部

處理衝突的幾種方法:#

一、 開放定址法#

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表當衝突多的時候資料容易堆聚在一起,這時候對查詢不友好;
  • 刪除結點不能簡單地將被刪結 點的空間置為空,否則將截斷在它之後填人散列表的同義詞結點的查詢路徑。因此在 用開放地址法處理衝突的散列表上執行刪除操作,只能在被刪結點上做刪除標記,而不能真正刪除結點
  • 當空間滿了,還要建立一個溢位表來存多出來的元素。

二、再雜湊法#

Hi = RHi(key),i=1,2,...k
RHi均是不同的雜湊函式,即在同義詞產生地址衝突時計算另一個雜湊函式地址,直到不發生衝突為止。這種方法不易產生聚集,但是增加了計算時間。

缺點:增加了計算時間。

三、建立一個公共溢位區#

假設雜湊函式的值域為[0,m-1],則設向量HashTable[0...m-1]為基本表,每個分量存放一個記錄,另設立向量OverTable[0....v]為溢位表。所有關鍵字和基本表中關鍵字為同義詞的記錄,不管他們由雜湊函式得到的雜湊地址是什麼,一旦發生衝突,都填入溢位表。

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

四、鏈地址法(拉鍊法)#

將所有關鍵字為同義詞的記錄儲存在同一線性連結串列中,也就是把衝突位置的元素構造成連結串列。

拉鍊法的優點:

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

拉鍊法的缺點:

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

回到頂部

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的冪次方。

關於泊淞分佈的知識請看泊松分佈和指數分佈:10分鐘教程

參考:為什麼Java中的HashMap預設載入因子是0.75