java 散列運算淺分析 hash()


        文章部分代碼圖片和總結來自參考資料

哈希和常用的方法

         散列,從中文字面意思就很好理解了,分散排列,我們知道數組地址空間連續,查找快,增刪慢,而鏈表,查找慢,增刪快,兩者結合起來形成散列表。如下圖。

 

hash

 

         常見的hash 散列方法有 :

 

直接定址法:直接以關鍵字k或者k加上某個常數(k+c)作為哈希地址。

數字分析法:提取關鍵字中取值比較均勻的數字作為哈希地址。(ThreadLocalMap中取的斐波那契數列數 0x61c88647 )

除留余數法:用關鍵字k除以某個不大於哈希表長度m的數p,將所得余數作為哈希表地址。

分段疊加法:按照哈希表地址位數將關鍵字分成位數相等的幾部分,其中最后一部分可以比較短。然后將這幾部分相加,舍棄最高進位后的結果就是該關鍵字的哈希地址。

平方取中法:如果關鍵字各個部分分布都不均勻的話,可以先求出它的平方值,然后按照需求取中間的幾位作為哈希地址。

偽隨機數法:采用一個偽隨機數當作哈希函數。

 

        散列后難免有碰撞 ,下面是解決碰撞的方法  :

 

開放定址法

開放定址法就是一旦發生了沖突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。(ThreadLoalMap)

 

鏈地址法

將哈希表的每個單元作為鏈表的頭結點,所有哈希地址為i的元素構成一個同義詞鏈表。即發生沖突時就把該關鍵字鏈在以該單元為頭結點的鏈表的尾部。(hashMap)

 

再哈希法

當哈希地址發生沖突用其他的函數計算另一個哈希函數地址,直到沖突不再產生為止。

 

建立公共溢出區

將哈希表分為基本表和溢出表兩部分,發生沖突的元素都放入溢出表中。

 

 

源碼解析

        我們知道Object類型有個hashCode()方法,那么假如讓你設計散列表,我們的直接想法肯定是直接對象的hashCode()對面謳歌數取模就夠了,為什么呢,首先理解一下取模,以我們的時鍾為例,12 個指針,13對12 取模就是1 (相當於下午1時),同理,要是24小時,對12取模,得到地數值很均勻。我們知道取模操作是“%”,處於效率考慮,一般使用位運算來代替(&運算)。

 

X % 2^n = X & (2^n - 1)

2^n表示2的n次方,也就是說,一個數對2^n取模 == 一個數和(2^n - 1)做按位與運算 。

假設n為3,則2^3 = 8,表示成2進制就是1000。2^3 -1 = 7 ,即0111。

此時X & (2^3 - 1) 就相當於取X的2進制的最后三位數。

從2進制角度來看,X / 8相當於 X >> 3,即把X右移3位,此時得到了X / 8的商,而被移掉的部分(后三位),則是X % 8,也就是余數。

 

         下面看源碼分析。

 

HashMap

 

hashmapinJDK7

 

         hash方法計算出hash值,而indexFor 計算出該元素在散列表中的位置。indexFor方法很好理解啊,就是上面的(2的N次方-1),當時hash方法的一波操作是什么意思呢?我們來看不同的三個數的hash值。

 

hash沖突

         可以看到后兩個數的高位不同,低位相同,產生hash沖突,下圖是后兩個數經過一波操作后,得到的hash值,可以看到經過這樣的操作就解決了沖突。 我們思考一下為什么會沖突,是因為 &運算,0和誰&都會得到0

hash沖2

        於是我們得出了如下結論

 

   這段代碼是為了對key的hashCode進行擾動計算,防止不同hashCode的高位不同但低位相同導致的hash沖突。簡單點說,就是為了把高位的特征和低位的特征組合起來,降低哈希沖突的概率,也就是說,盡量做到任何一位的變化都能對最終得到的結果產生影響。

 

        同時還有一點,Object的hashCode方法會有負數,hashmap使用位運算,得到的hash值都是正整數(可以想一下為什么)

 

 

HashTable in Java7

        我們在JDK1.8 中 HashTable 的put方法中看到 :

  1         int hash = key.hashCode();
  2         int index = (hash & 0x7FFFFFFF) % tab.length;

         前面的 ‘0 & 0x7FFFFFFF’ 是去絕對值的意思,后面直接取模了。需要注意的是 HashTable 默認的初始大小為11,之后每次擴充為原來的2n+1,也就是說,HashTable的鏈表數組的默認大小是一個素數、奇數。之后的每次擴充結果也都是奇數。由於HashTable會盡量使用素數、奇數作為容量的大小。當哈希表的大小為素數時,簡單的取模哈希的結果會更加均勻。可參考:http://zhaox.github.io/algorithm/2015/06/29/hash

 

 

ConcurrentHashMap In Java 7

 

concurrenthashmap

          有了上面 hashmap的擾動運算的介紹,應該很好理解了。

 

 

HashMap In Java 8

         直接的鏈地址法,就是沖突了就在后面增加一個節點的方法有什么壞處呢?在最壞的情況下,這種方式會將HashMap的get方法的性能從 O(1)降低到 O(n)----有可能所有的數據生成在一條鏈表,即每個都沖突。Java 8中使用平衡樹來替代鏈表存儲沖突的元素。這意味着我們可以將最壞情況下的性能從 O(n)提高到 O(logn)

         我們再看一下hash函數。

 

hashmapin8

 

在JDK1.8的實現中,優化了高位運算的算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的。以上方法得到的int的hash值,然后再通過 h&(table.length-1)來得到該對象在數據中保存的位置。

 

 

ConcurrentHashMap In Java 8

 

conhashmapin8

 

  Java 8的ConcurrentHashMap作者認為引入紅黑樹后,即使哈希沖突比較嚴重,尋址效率也足夠高,所以作者並未在哈希值的計算上做過多設計,只是將Key的hashCode值與其高16位作異或並保證最高位為0(從而保證最終結果為正整數)。

 

 

參數資料 :


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM