數據結構精講:從原理到實戰–學習筆記04
本筆記是記錄學習 《數據結構精講:從原理到實戰》,作者是:蔡元楠,Google Brain資深工程師。
如有侵權,聯繫刪除!
哈希表與哈希函數
哈希表,其實本質上是一個數組。哈希函數的定義是將任意長度的一個對象映射到一個固定長度的值上,而這個值我們可以稱作是哈希值(Hash Value)。
哈希函數一般會有以下三個特性:
-
任何對象作爲哈希函數的輸入都可以得到一個相應的哈希值;
-
兩個相同的對象作爲哈希函數的輸入,它們總會得到一樣的哈希值;
-
兩個不同的對象作爲哈希函數的輸入,它們不一定會得到不同的哈希值。
按照 Java String 類裏的哈希函數公式(即下面的公式)來計算出不同字符串的哈希值。String 類裏的哈希函數是通過 hashCode 函數來實現的,這裏假設哈希函數的字符串輸入爲 s,所有的字符串都會通過以下公式來生成一個哈希值:
在什麼樣的情況下會體現出哈希函數的第三種特性呢?我們再來看看下面這個例子。現在我們想要計算字符串 “Aa” 和 “BB” 的哈希值,還是繼續套用上面的的公式。
“Aa” 的哈希值爲:
"Aa" = 'A' * 31 + 'a' = 65 * 31 + 97 = 2112
“BB” 的哈希值爲:
"BB" = 'B' * 31 + 'B' = 66 * 31 + 66 = 2112
可以看到,不同的兩個字符串其實是會輸出相同的哈希值出來的,這時候就會造成哈希碰撞
需要注意的是,雖然 hashCode 的算法裏都是加法,但是算出來的哈希值有可能會是一個負數。
我們都知道,在計算機裏,一個 32 位 int 類型的整數裏最高位如果是 0 則表示這個數是非負數,如果是 1 則表示是負數。
如果當字符串通過計算算出的哈希值大於 232-1 時,也就是大於 32 位整數所能表達的最大正整數了,則會造成溢出,此時哈希值就變爲負數了。
hashCode 函數中的“魔數”(Magic Number)
細心的你一定發現了,上面所講到的 Java String 類裏的 hashCode 函數,一直在使用一個 31 這樣的正整數來進行計算,這是爲什麼呢?下面一起來研究一下 Java Openjdk-jdk11 中 String.java 的源碼(源碼鏈接),看看這麼做有什麼好處。
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
可以看到,String 類的 hashCode 函數依賴於 StringLatin1 和 StringUTF16 類的具體實現。而 StringLatin1 類中的 hashCode 函數(源碼鏈接)和 StringUTF16 類中的 hashCode 函數(源碼鏈接)所表達的算法其實是一致的。
StringLatin1 類中的 hashCode 函數如下面所示:
public static int hashCode(byte[] value) {
int h = 0;
for (byte v : value) {
h = 31 * h + (v & 0xff);
}
return h
StringUTF16 類中的 hashCode 函數如下面所示:
public static int hashCode(byte[] value) {
int h = 0;
int length = value.length >> 1;
for (int i = 0; i < length; i++) {
h = 31 * h + getChar(value, i);
}
return h
一個好的哈希函數算法都希望儘可能地減少生成出來的哈希值會造成哈希碰撞的情況。
Goodrich 和 Tamassia 這兩位計算機科學家曾經做過一個實驗,他們對超過 50000 個英文單詞進行了哈希值運算,並使用常數 31、33、37、39 和 41 作爲乘數因子,每個常數所算出的哈希值碰撞的次數都小於 7 個。但是最終選擇 31 還是有着另外幾個原因。
從數學的角度來說,選擇一個質數(Prime Number)作爲乘數因子可以讓哈希碰撞減少。其次,我們可以看到在上面的兩個 hashCode 源碼中,都有着一條 31 * h 的語句,這條語句在 JVM 中其實都可以被自動優化成“(h << 5) - h”這樣一條位運算加上一個減法指令,而不必執行乘法指令了,這樣可以大大提高運算哈希函數的效率。
所以最終 31 這個乘數因子就被一直保留下來了。