哈希表
我們通過將我們要查找的某種數據類型轉化爲一個索引index,然後通過索引去數組中查找,這時它的複雜度就是O(1)級別的。而將某個數據類型轉化爲索引的函數我們就稱爲是哈希函數,比如說將26個小寫字母轉化爲索引,我們可以這麼寫
index = ch - 'a';
這樣就建立起了一一對應的關係,但是並不是所有的對應關係都是一一對應的,因爲數組的容量是有限的,而輸入的範圍可能是無窮的,所以很有可能不同的鍵對應着同一個索引,比如說鍵是字符串,因爲字符串的組合方式是非常的多,可以看做是無窮的,我們不可能去開闢一個無窮的空間去與這些字符串一一對應,所以不同的字符串生成的索引很有可能會有衝突,我們稱這種情況爲哈希衝突。我們定義我們在上面提到的索引叫做哈希值,由於上面講到的哈希衝突,所以我們要設計好哈希函數(hashCode())使得發生哈希衝突的可能性小,即使哈希函數產生的哈希值均勻的分佈在數組中。
哈希函數的設計
哈希函數應該滿足上面提到的:哈希函數產生的哈希值均勻的分佈在數組中。數據的類型五花八門,對於特殊的領域有特殊領域的哈希函數的設計方式,甚至還有專門的論文,說這麼多就是想說哈希函數的設計十分的複雜,在這裏我們只提最簡單的一種,哈希函數的設計應該滿足
- 一致性
- 如果a == b,那麼hashCode(a) == hashCode(b)
- 高效性
- 計算迅速
- 均勻性
- 輸出儘可能均勻
由於Java中基本數據類型和字符串類型有默認的hashCode()計算,所以我們就用Java自帶的hashCode計算基本數據類型和字符串的哈希值,而對於引用類型Java是根據地址計算的哈希值,所以可能會出現問題,需要我們自己自定義規則,比如對於一個Student類,我們規定學號以及姓名相同(不區分大小寫)就是同一個學生,所以根據一致性原則,它們應該產生相同的哈希值,但是由於Java默認是根據地址產生哈希值,由於二者的地址是不同的,所以產生的哈希值有極大的概率是不同的,所以我們需要自己創建哈希函數。
鏈地址法
現在我們來演示往哈希表中添加元素的步驟
import java.util.TreeMap;
public class HashTable<K, V> {
//數組中存儲的是TreeMap這種查找表
private TreeMap<K, V>[] hashTable;
private int M;
private int size;
public HashTable(int M) {
this.M = M;
size = 0;
hashTable = new TreeMap[M];
for (int i = 0; i < hashTable.length; i++) {
hashTable[i] = new TreeMap<>();
}
}
public HashTable() {
this(97);
}
public int getSize() {
return size;
}
//得到在數組中的索引
private int hash(K key) {
//與0x7fffffff是爲了消除負數
return (key.hashCode() & 0x7fffffff) % M;
}
public void add(K key, V value) {
TreeMap<K, V> map = hashTable[hash(key)];
//先查看已經是否有這個鍵了
if (map.containsKey(key)) {
//有則更新
map.put(key, value);
} else {
//沒有則進行添加,並維護size
map.put(key, value);
size++;
}
}
public V remove(K key, V value) {
V ret = null;
TreeMap<K, V> map = hashTable[hash(key)];
//如果包含鍵則刪除,沒有返回null
if (map.containsKey(key)) {
ret = map.remove(key);
size--;
}
return ret;
}
public void set(K key, V value) {
TreeMap<K, V> map = hashTable[hash(key)];
//沒有該鍵拋出異常
if (!map.containsKey(key)) {
throw new IllegalArgumentException("鍵不存在");
}
map.put(key,value);
}
//直接得到相應的TreeMap,然後去查,TreeMap有檢查步驟
public V get(K key) {
return hashTable[hash(key)].get(key);
}
}
import java.util.TreeMap;
public class HashTable<K, V> {
private static final int upperTol = 10;
private static final int lowerTol = 2;
private static final int initCapacity = 7;
//數組中存儲的是TreeMap這種查找表
private TreeMap<K, V>[] hashTable;
private int M;
private int size;
public HashTable(int M) {
//只顯示改變的內容
//...
hashTable = new TreeMap[initCapacity];
}
public void add(K key, V value) {
//...
if (size >= upperTol * M) {
resize(2 * M);
}
}
public V remove(K key, V value) {
//...
if (size < M * lowerTol && M / 2 >= initCapacity) {
resize(M / 2);
}
return ret;
}
private void resize(int newM) {
TreeMap<K,V>[] newHashTable = new TreeMap[newM];
//後面要更新M,但是還需要舊M遍歷數組
int oldM = M;
//由於後面要重新計算下標,所以這裏要更新M
M = newM;
for (int i = 0; i < oldM; i++) {
TreeMap<K, V> map = hashTable[i];
for (K key: map.keySet()) {
//重新計算下標並賦值
newHashTable[hash(key)].put(key, map.get(key));
}
}
hashTable = newHashTable;
}
}
但是我們發現每次我們都擴容爲2 * M,這時M就不是一個素數了,爲了解決這一個問題,我們準備一個素數表,讓M取素數表中的值,每次擴容M在素數表中的索引+1,縮容-1
import java.util.TreeMap;
public class HashTable<K, V> {
//素數表
private static final int[] capacity = {};
private static final int upperTol = 10;
private static final int lowerTol = 2;
private int capacityIndex = 0;
//數組中存儲的是TreeMap這種查找表
private TreeMap<K, V>[] hashTable;
private int M;
private int size;
public HashTable() {
this.M = capacity[capacityIndex];
size = 0;
hashTable = new TreeMap[M];
for (int i = 0; i < hashTable.length; i++) {
hashTable[i] = new TreeMap<>();
}
}
public void add(K key, V value) {
//...
if (size >= upperTol * M && capacityIndex + 1 < size) {
capacityIndex++;
resize(capacity[capacityIndex]);
}
}
public V remove(K key, V value) {
//...
if (size < M * lowerTol && capacityIndex - 1 >= 0) {
capacityIndex--;
resize(capacity[capacityIndex]);
}
return ret;
}
}
完整代碼
import java.util.TreeMap;
public class HashTable<K, V> {
private static final int[] capacity = {};
private static final int upperTol = 10;
private static final int lowerTol = 2;
private int capacityIndex = 0;
//數組中存儲的是TreeMap這種查找表
private TreeMap<K, V>[] hashTable;
private int M;
private int size;
public HashTable() {
this.M = capacity[capacityIndex];
size = 0;
hashTable = new TreeMap[M];
for (int i = 0; i < hashTable.length; i++) {
hashTable[i] = new TreeMap<>();
}
}
public int getSize() {
return size;
}
//得到在數組中的索引
private int hash(K key) {
//與0x7fffffff是爲了消除負數
return (key.hashCode() & 0x7fffffff) % M;
}
public void add(K key, V value) {
TreeMap<K, V> map = hashTable[hash(key)];
//先查看已經是否有這個鍵了
if (map.containsKey(key)) {
//有則更新
map.put(key, value);
} else {
//沒有則進行添加,並維護size
map.put(key, value);
size++;
}
if (size >= upperTol * M && capacityIndex + 1 < capacity.length) {
capacityIndex++;
resize(capacity[capacityIndex]);
}
}
public V remove(K key, V value) {
V ret = null;
TreeMap<K, V> map = hashTable[hash(key)];
//如果包含鍵則刪除,沒有返回null
if (map.containsKey(key)) {
ret = map.remove(key);
size--;
}
if (size < M * lowerTol && capacityIndex - 1 >= 0) {
capacityIndex--;
resize(capacity[capacityIndex]);
}
return ret;
}
public void set(K key, V value) {
TreeMap<K, V> map = hashTable[hash(key)];
//沒有該鍵拋出異常
if (!map.containsKey(key)) {
throw new IllegalArgumentException("鍵不存在");
}
map.put(key,value);
}
//直接得到相應的TreeMap,然後去查,TreeMap有檢查步驟
public V get(K key) {
return hashTable[hash(key)].get(key);
}
private void resize(int newM) {
TreeMap<K,V>[] newHashTable = new TreeMap[newM];
//後面要更新M,但是還需要舊M遍歷數組
int oldM = M;
//由於後面要重新計算下標,所以這裏要更新M
M = newM;
for (int i = 0; i < oldM; i++) {
TreeMap<K, V> map = hashTable[i];
for (K key: map.keySet()) {
//重新計算下標並賦值
newHashTable[hash(key)].put(key, map.get(key));
}
}
hashTable = newHashTable;
}
}