本文咱們來探討一個數據結構的基礎話題:hash 結構中的開放地址法(Open Addressing)java
HashMap 無 Java 人不知無 Java 人不曉,它使用開鏈法處理 hash 碰撞,將碰撞的元素用鏈表串起來掛在第一維數組上。可是並非全部語言的字典都使用開鏈法搞定的,好比 Python,它使用的是另外一種形式 —— 開放地址法。相比 HashMap 是二維的結構,它只是一維的,只有一個數組。python
開放地址法與開鏈法的不一樣之處在於如何處理 hash 衝突。當新來一個元素哈希到數組中的位置已經被其它元素佔據了該怎麼辦?數組
開放地址法會根據當前的位置計算出下一個位置,將這個衝突的元素挪進來。若是這下一個位置也被佔用了,那麼就再計算下一個位置,直到找到一個空的位置。能夠想像,將會有一條虛擬的鏈條將這些相關的位置串起來。這個虛擬的鏈條就比如開鏈法裏面的第二維鏈表。只不過鏈表有顯示的指針字段,而虛擬鏈條沒有,它的這個鏈條徹底是經過數學函數計算出來的。數據結構
root = hash(key) % m // 第一個位置,m 爲數組的長度
index_i = (root + p(key, i)) % m // 鏈條中的第 i 個位置
index_1 = (root + p(key, 1)) % m
index_2 = (root + p(key, 2)) % m
...
複製代碼
這個數學函數就是上面代碼中的 p —— probe sequence (探測序列)。尋找空位置的過程就是一步一步的探測的過程。不一樣的 key 會生成不同的探測序列。函數
在查找的時候,若是第一個位置上保存的 key 不是目標 key,那就沿着探測路徑繼續尋找,直到找到或者遇到一個空位置爲止。性能
到這裏你可能會擔憂又沒有可能探測過程會出現死循環,探來探去又回到原點了,或者是回到路徑的中間。這是頗有可能的,因此這裏的探測函數不能隨意選擇,它必須保證探測序列不會出現循環,通過 m-1 次探測生成的探測序列必須正好是 1..m-1的全排列。spa
這樣的探測函數有不少,其中最多見的一種是線性探測函數。該探測序列和輸入 key 無關。最終的探測路徑只和初始位置相關。指針
// m = 2^n,c 必須是一個奇數
p(key, i) = c * i
index_i = root + c * i
複製代碼
這裏我不去仔細證實這個函數爲何知足要求,咱們能夠寫個簡單的代碼來驗證一下。code
public class HashTest {
public static void main(String[] args) {
int m = 1 << 16;
int c = 111111;
Set<Integer> nums = new HashSet<>();
for (int i = 1; i < m; i++) {
int p = c * i % m;
if(nums.contains(p)) {
System.out.println("duplicated");
return;
}
nums.add(p);
}
System.out.println("no duplicate");
}
}
------------
no duplicate
複製代碼
好,死循環的問題解決了。下面還有一個問題,刪除該怎麼辦?開鏈法刪除就很簡單,直接從鏈表中摘走就是,可是開放地址法就不是那麼好辦,你不能隨意地將探測路徑中的某個元素刪除,這樣會致使探測路徑中斷。cdn
爲了避免讓探測路徑中斷,刪除有兩種實現方案
在刪除的位置置一個特殊的刪除標記,查找時能夠直接跳過繼續沿着探測路徑日後尋找。須要注意的是這個刪除的位置在後續的新元素插入時會獲得回收利用。插入元素時,遍歷探測路徑,遇到了第一個刪除標記的位置,這時不能當即插入。由於有可能這個元素存在於探測路徑的後半部。因此須要遍歷到底若是發現確實路徑裏不存在這個元素,這時候要回過頭來插入到第一個發現刪除標記的位置。若是刪除的位置過多,會影響查找和插入性能。 將探測路徑的後半部元素所有刪除,而後從新插入。若是路徑較長,可能會影響插入性能。 到這裏彷佛就結束了,其實還有一點咱們沒有注意到。那就是在採用 p(key, i) = c * i 探測函數的前提下,若是多個不一樣的 key 哈希的第一個位置相同,那麼它們將會共享同一條探測路徑。由於探測路徑徹底由第一個位置來決定的,和 輸入 key 無關。那麼這些相關的 key 就會在一條探測路徑上彙集,這可能會致使數據分佈的結果不那麼均勻。
若是咱們使用一個不一樣的探測函數,使得它和輸入 key 相關,那麼就能夠消滅這個彙集問題。咱們能夠將探測函數中的常量 c 換成一個 hash 函數,只要這個函數老是返回奇數就能夠了,這樣的 hash 函數仍是很是容易編寫的。
p(key, i) = h2(key) * i
複製代碼
總算結束了,讀者們,大家還有什麼須要補充的麼?