集合框架——hashCode算法實現

目錄

一、hashCode算法實現

二、HashMap中爲什麼數組的長度爲2的冪次方

三、HashMap 在計算bucket位置時,爲什麼使用 & 與運算代替模運算?

四、自定義 HashMap 容量最好是多少?

五、如何解決Hash衝突


hashcode事一個int類型的數字,從Object的hashCode()方法的註釋中可以看出hashcode主要是用於hashMap等類用於尋址的一個參數

一、hashCode算法實現

1、對象默認的hashCode()方法:對象如果沒有重寫hashCode()方法,那麼就是繼承Object的hashCode()方法,這是一個本地(native)方法,返回實例對象的內存地址(註釋中有說明)

//return distinct integers for distinct objects. (This is typically implemented by 
//converting the internal address of the object into an integer
public native int hashCode();

2、String類的hashCode()方法:String在內存中是以char數組的形式存儲的,hashCode()方法是對每個char字符運算得到

//private final char value[];
public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

至於爲什麼使用31,在《Effective Java》中給出瞭解釋:

因爲31是一個奇素數。如果乘數是偶數,並且乘法溢出的話,信息就會丟失,因爲與2相乘等價於移位運算(低位補0)。使用素數的好處並不很明顯,但是習慣上使用素數來計算散列結果。 31 有個很好的性能,即用移位和減法來代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i, 現代的 VM 可以自動完成這種優化。

3、HashMap的hashCode()方法:hashMap的hashCode是對map中所有Entry的hashCode累加的結果

//(AbstractMap類中)
public int hashCode() {
        int h = 0;
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext())
            h += i.next().hashCode();
        return h;
    }

而Entry的hashCode計算如下: 

//Entry的hashCode(HashMap類中)
public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
//Objects類的hashCode
public static int hashCode(Object o) {
        return o != null ? o.hashCode() : 0;
    }
//而Objects的hashCode又調用了object的hashCode,如果沒有重寫,將返回對象的內存地址

 而在map的put和get操作中,是通過hash(Object key)來對Entry進行bucket(桶)的尋址:


static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

至於爲什麼要使用高、低16位進行異或操作,註釋中也給出瞭解釋:

因爲我們可以看出,計算bucket的位置的算法是通過與數組長度做與運算的得到的:(length - 1) & hash,而length一般取2的冪次方,因此(length - 1)的二進制就是一個高位爲0,低位爲1的掩模,也就是說高位的信息是沒有得到利用的,爲了充分使用hash的高位信息,就有了高位右移的操作,而高、低位慾火操作時綜合考慮了信息的使用效率和運算效率採取的方法。

二、HashMap中爲什麼數組的長度爲2的冪次方

原因同上,長度16或其他2的冪次方,(Length - 1)可以形成一個高位爲0,低位爲1的掩模,這種情況下,Index的結果等於hashCode的最後幾位。只要輸入的hashCode本身符合均勻分佈,Hash算法的結果就是均勻的。但是如果是10之類的非2的冪次方的數字,經過大量的驗證發現(length - 1) & hash的結果很容易相同,而結果就是大量的node被插入同一個bucket中,導致形成鏈表,map的性能下降。

三、HashMap 在計算bucket位置時,爲什麼使用 & 與運算代替模運算?

HashMap的getValue()方法在計算bucket位置時,使用了(n - 1) & hash]來計算node在tab數組中的位置,這是因爲對於現代的處理器來說,除法和求餘數(模運算)是最慢的動作,而當b是2的指數時,等式a % b == (b-1) & a 成立,因此爲了提高運算效率,使用與運算代替了模運算。

四、自定義 HashMap 容量最好是多少?

如果沒有自定義初始化容量,那麼hashMap的默認初始化容量是16,但是如果我們想要自己設計map的容量呢?

通過源碼就會發現,如果Map中已有數據的容量達到了初始容量的 75%,那麼散列表就會擴容,而擴容將會重新將所有的數據重新散列,性能損失嚴重,所以,我們可以必須要大於我們預計數據量的 1.34 倍,同時容量滿足2的冪次方,也就是說初始容量是預估容量的1.34倍,然後向上找一個2的冪次方。

五、如何解決Hash衝突

解決hash衝突的方法有四個:鏈地址法( 拉鍊法)、開放地址法、再哈希法和建立公共溢出區法

  • 鏈地址法:將相同hashCode的元素鏈接在一個鏈表中

HashMap就是數組+鏈表的實現方式,也就是通過“鏈地址法”來解決hash衝突,如果hash衝突太嚴重,則會導致鏈表很長,導致map的性能急劇下降,因此隨後hashMap又使用了數組+鏈表+紅黑樹的實現方式,當鏈表的長度超過8,那麼採用紅黑樹來保存node元素。

優點:
1、對於記錄總數頻繁可變的情況,處理的比較好(也就是避免了動態調整,就是全部再hash的開銷) 
2、由於記錄存儲在鏈表結點中,不會造成內存的浪費,如果記錄本身size很大,指針的開銷可以忽略不計
3、刪除記錄時,比較方便,直接通過指針操作即可

缺點: 
1、存儲的記錄是隨機分佈在內存中的,在查詢記錄時,相比結構緊湊的數據類型(比如數組),哈希表的跳轉訪問會帶來額外的時間開銷 
2、如果記錄數量是已知的,並不會發生變化,可以創建一個不會產生衝突的完美哈希函數,此時封閉散列的性能將遠高於開放散列 
3、由於使用指針,記錄不容易進行序列化(serialize)操作
  • 開放地址法:當關鍵字key的哈希地址p=H(key)出現衝突時,以p爲基礎,產生另一個哈希地址p1,如果p1仍然衝突,再以p爲基礎,產生另一個哈希地址p2,…,直到找出一個不衝突的哈希地址pi ,將相應元素存入其中。對應的計算公式爲:

H_i = (H(key)+d_i)%m  或者   H_i = H(key+d_i)%m,i=1,2,…,n,

其中i表示第i次hash計算,H()表示hash方法,d_i表示增量,根據增量的不同產生方式,又可以分爲:

線性探測再散列

d_i=1,2,3,…,m-1

這種方法的特點是:衝突發生時,順序查看錶中下一單元,直到找出一個空單元或查遍全表。

二次探測再散列

d_i=1^2-1^22^2-2^23^2-3^2,…,,k^2-k^2    ( k<=m/2 )

這種方法的特點是:衝突發生時,在表的左右進行跳躍式探測,比較靈活。

僞隨機探測再散列

具體實現時,應建立一個僞隨機數發生器,,並給定一個隨機數做起點,產生一個僞隨機數序列,如d_i=(i+p) % m。然後利用公式進行再hash。

這裏會奇怪使用開放地址法怎麼查找正確的數據,查找數據時會比較hash值和key本身,二者都相同纔會對其進行操作,如果hash值相同而key值不同,那麼則繼續進行hash計算。

優點: 
1、記錄更容易進行序列化(serialize)操作(沒有指針)
2、如果記錄總數可以預知,可以創建完美哈希函數,此時處理數據的效率是非常高的

缺點: 
1、記錄的數目不能超過桶數組的長度,如果超過就需要擴容,而擴容會導致某次操作的時間成本飆升
2、使用探測序列,有可能由於衝突次數太多導致計算的時間變長,導致哈希表的處理性能降低 
3、由於記錄是存放在桶數組中的,而桶數組必然存在空槽,空槽佔用的空間會導致明顯的內存浪費 
4、刪除記錄比較麻煩。比如需要刪除記錄a,記錄b是在a之後插入桶數組的,但是和記錄a有衝突,是通過探測序列再次跳轉找到的地址,所以如果直接刪除a,a的位置變爲空槽,而空槽是查詢記錄失敗的終止條件,這樣會導致記錄b在a的位置重新插入數據前不可見,所以不能直接刪除a,而是設置刪除標記。這就需要額外的空間和操作
  • 再哈希法:同時構造多個不同的哈希函數\small H_i(key),當哈希地址\small hash=H_1(key)發生衝突時,再計算\small hash=H_2(key)……,直到衝突不再產生。

這種方法不易產生聚集,但增加了計算時間。

  • 建立公共溢出區法:將哈希表分爲基本表和溢出表兩部分,凡是和基本表發生衝突的元素,一律填入溢出表。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章