1. 程式人生 > 其它 >雜湊函式與雜湊衝突

雜湊函式與雜湊衝突

什麼是 Hash

Hash(雜湊),又稱“雜湊”。在某種程度上,雜湊是與排序相反的一種操作,排序是將集合中的元素按照某種方式比如字典順序排列在一起。而雜湊通過計算雜湊值,打破元素之間原有的關係,使集合中的元素按照雜湊函式的分類進行排列。

在介紹一些集合時,我們總強調需要重寫某個類的 equlas() 方法和 hashCode() 方法,確保唯一性。這裡的 hashCode() 表示的是對當前物件的唯一標示。計算 hashCode 的過程就稱作 雜湊。

為什麼要有 Hash

我們通常使用陣列或者連結串列來儲存元素,一旦儲存的內容數量特別多,需要佔用很大的空間,而且在查詢某個元素是否存在的過程中,陣列和連結串列都需要挨個迴圈比較,而通過 雜湊 計算,可以大大減少比較次數。

舉個栗子: 現在有 4 個數 {2,5,9,13},需要查詢 13 是否存在。

1.使用陣列儲存,需要新建個數組 new int[]{2,5,9,13},然後需要寫個迴圈遍歷查詢:

int[] numbers = new int[]{2,5,9,13};
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] == 13){
            System.out.println("find it!");
            return;
        }
}

這樣需要遍歷 4 次才能找到,時間複雜度為 O(n)。

2.而假如儲存時先使用雜湊函式進行計算,這裡我隨便用個函式:

 H[key] = key % 3;

四個數 {2,5,9,13} 對應的雜湊值為:

 H[2] = 2 % 3 = 2;
 H[5] = 5 % 3 = 2;
 H[9] = 9 % 3 = 0;
 H[13] = 13 % 3 = 1;

然後把它們儲存到對應的位置。

當要查詢 13 時,只要先使用雜湊函式計算它的位置,然後去那個位置檢視是否存在就好了,本例中只需查詢一次,時間複雜度為 O(1)。

因此可以發現,雜湊 其實是隨機儲存的一種優化,先進行分類,然後查詢時按照這個物件的分類去找。

雜湊通過一次計算大幅度縮小查詢範圍,自然比從全部資料裡查詢速度要快。

雜湊函式

雜湊的過程中需要使用雜湊函式進行計算。

雜湊函式是一種對映關係,根據資料的關鍵詞 key ,通過一定的函式關係,計算出該元素儲存位置的函式。

表示為:

address = H [key]

幾種常見的雜湊函式(雜湊函式)構造方法

  • 直接定址法 
    • 取關鍵字或關鍵字的某個線性函式值為雜湊地址。
    • 即 H(key) = key 或 H(key) = a*key + b,其中a和b為常數。
  • 除留餘數法 
    • 取關鍵字被某個不大於散列表長度 m 的數 p 求餘,得到的作為雜湊地址。
    • 即 H(key) = key % p, p < m。 
  • 數字分析法 
    • 當關鍵字的位數大於地址的位數,對關鍵字的各位分佈進行分析,選出分佈均勻的任意幾位作為雜湊地址。
    • 僅適用於所有關鍵字都已知的情況下,根據實際應用確定要選取的部分,儘量避免發生衝突。
  • 平方取中法 
    • 先計算出關鍵字值的平方,然後取平方值中間幾位作為雜湊地址。
    • 隨機分佈的關鍵字,得到的雜湊地址也是隨機分佈的。
  • 摺疊法(疊加法) 
    • 將關鍵字分為位數相同的幾部分,然後取這幾部分的疊加和(捨去進位)作為雜湊地址。
    • 用於關鍵字位數較多,並且關鍵字中每一位上數字分佈大致均勻。 
  • 隨機數法 
    • 選擇一個隨機函式,把關鍵字的隨機函式值作為它的雜湊值。
    • 通常當關鍵字的長度不等時用這種方法。 

構造雜湊函式的方法很多,實際工作中要根據不同的情況選擇合適的方法,總的原則是儘可能少的產生衝突。

通常考慮的因素有關鍵字的長度和分佈情況、雜湊值的範圍等。

如:當關鍵字是整數型別時就可以用除留餘數法;如果關鍵字是小數型別,選擇隨機數法會比較好。

雜湊衝突的解決

選用雜湊函式計算雜湊值時,可能不同的 key 會得到相同的結果,一個地址怎麼存放多個數據呢?這就是衝突。

常用的主要有兩種方法解決衝突:

1.連結法(拉鍊法)

拉鍊法解決衝突的做法是: 
將所有關鍵字為同義詞的結點連結在同一個單鏈表中。

若選定的散列表長度為 m,則可將散列表定義為一個由 m 個頭指標組成的指標陣列 T[0..m-1] 。

凡是雜湊地址為 i 的結點,均插入到以 T[i] 為頭指標的單鏈表中。 
T 中各分量的初值均應為空指標。

在拉鍊法中,裝填因子 α 可以大於 1,但一般均取 α ≤ 1。

2.開放定址法

用開放定址法解決衝突的做法是:

用開放定址法解決衝突的做法是:當衝突發生時,使用某種探測技術在散列表中形成一個探測序列。沿此序列逐個單元地查詢,直到找到給定的關鍵字,或者碰到一個開放的地址(即該地址單元為空)為止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查詢時探測到開放的地址則表明表中無待查的關鍵字,即查詢失敗。

簡單的說:當衝突發生時,使用某種探查(亦稱探測)技術在散列表中尋找下一個空的雜湊地址,只要散列表足夠大,空的雜湊地址總能找到。

按照形成探查序列的方法不同,可將開放定址法區分為線性探查法、二次探查法、雙重雜湊法等。

a.線性探查法

hi=(h(key)+i) % m ,0 ≤ i ≤ m-1 

基本思想是: 
探查時從地址 d 開始,首先探查 T[d],然後依次探查 T[d+1],…,直到 T[m-1],此後又迴圈到 T[0],T[1],…,直到探查到 有空餘地址 或者到 T[d-1]為止。

b.二次探查法

hi=(h(key)+i*i) % m,0 ≤ i ≤ m-1 

基本思想是: 
探查時從地址 d 開始,首先探查 T[d],然後依次探查 T[d+1^2],T[d+2^2],T[d+3^2],…,等,直到探查到 有空餘地址 或者到 T[d-1]為止。

缺點是無法探查到整個雜湊空間。

c.雙重雜湊法

hi=(h(key)+i*h1(key)) % m,0 ≤ i ≤ m-1 

基本思想是: 
探查時從地址 d 開始,首先探查 T[d],然後依次探查 T[d+h1(d)], T[d + 2*h1(d)],…,等。

該方法使用了兩個雜湊函式 h(key) 和 h1(key),故也稱為雙雜湊函式探查法。

定義 h1(key) 的方法較多,但無論採用什麼方法定義,都必須使 h1(key) 的值和 m 互素,才能使發生衝突的同義詞地址均勻地分佈在整個表中,否則可能造成同義詞地址的迴圈計算。

該方法是開放定址法中最好的方法之一。

雜湊的應用

雜湊表(散列表)

雜湊表(hash table)是雜湊函式最主要的應用。

雜湊表是實現關聯陣列(associative array)的一種資料結構,廣泛應用於實現資料的快速查詢。

用雜湊函式計算關鍵字的雜湊值(hash value),通過雜湊值這個索引就可以找到關鍵字的儲存位置,即桶(bucket)。

雜湊表不同於二叉樹、棧、序列的資料結構一般情況下,在雜湊表上的插入、查詢、刪除等操作的時間複雜度是 O(1)。

查詢過程中,關鍵字的比較次數,取決於產生衝突的多少,產生的衝突少,查詢效率就高,產生的衝突多,查詢效率就低。

因此,影響產生衝突多少的因素,也就是影響查詢效率的因素。 
影響產生衝突多少有以下三個因素:

  1. 雜湊函式是否均勻;
  2. 處理衝突的方法;
  3. 雜湊表的載入因子。

雜湊表的載入因子和容量決定了在什麼時候桶數(儲存位置)不夠,需要重新雜湊。

載入因子太大的話桶太多,遍歷時效率變低;太大的話頻繁 rehash,導致效能降低。

所以載入因子的大小需要結合時間和空間效率考慮。

在 HashMap 中的載入因子為 0.75,即四分之三。

分散式快取

網路環境下的分散式快取系統一般基於一致性雜湊(Consistent hashing)。

簡單的說,一致性雜湊將雜湊值取值空間組織成一個虛擬的環,各個伺服器與資料關鍵字K使用相同的雜湊函式對映到這個環上,資料會儲存在它順時針“遊走”遇到的第一個伺服器。可以使每個伺服器節點的負載相對均衡,很大程度上避免資源的浪費。

在動態分散式快取系統中,雜湊演算法的設計是關鍵點。

使用分佈更合理的演算法可以使得多個服務節點間的負載相對均衡,可以很大程度上避免資源的浪費以及部分伺服器過載。

使用帶虛擬節點的一致性雜湊演算法,可以有效地降低服務硬體環境變化帶來的資料遷移代價和風險,從而使分散式快取系統更加高效穩定。