你受的苦,吃的虧,擔的責,扛的罪,忍的痛,到最後都會變成光,照亮你的路。
什麼是哈希表?
哈希表(Hash table,散列),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做哈希函數,存放記錄的數組叫做哈希表。
舉個栗子:
一個班有30名學生,他們的學號是1-30的,我們用數組來存儲這些學生:
學號 | 數組下標 |
---|---|
1 | 0 |
2 | 1 |
3 | 2 |
… | … |
30 | 29 |
事實上,這個數組就是一個哈希表,班裏每個學生的學號都對應了數組中的一個下標。
具體的對應關係爲 : 下標 = 學號 - 1
而 f(key) = value,給定一個鍵值,計算得到一個地址,這樣的關係函數就是哈希函數。
在這個具體的例子中, 下標 = 學號 - 1 就是哈希函數,給定一個學號,就能知道這個學生在數組中的存儲位置。這樣的話,想要查詢一個學生的信息只需要O(1)的時間複雜度!
當然這是一種最爲簡單的哈希表,想要使用哈希表的方法進行查找,就必須解決下面兩個問題:
- Hash函數的構建
- Hash衝突的解決方式
所謂的Hash衝突是指不同的key通過Hash函數所求的地址值value相同,則這些key就產生了Hash衝突
Hash函數的構建方法
哈希表之所以能達到O(1)的複雜度,本質上是以空間換時間。如果空間足夠大,相應的我們就能在O(1)的複雜度下完成各項操作,而如果只有O(1)的空間,那麼就需要O(n) (線性表)的時間複雜度。
Hash表就是時間和空間之間的平衡,因此Hash函數的構建是非常重要的。通常應該遵循下列原則:
- 高效性:hash函數本身運算儘量簡單,便與運算
- 一致性:若a = b ,則 hash(a)= hash(b)
- 均勻性:通過Hash函數得到的hash函數值必須在Hash地址範圍內,且分佈均勻,地址衝突儘量小
在上一個學號的例子中,我們直接以 下標 = 學號 - 1 的方式,很快的完成了Hash函數的構建,但事實上不是所有的問題都可以如此簡單的構建出來,下面就來討論更復雜的情況下如何構建Hash函數。
- 大整數:除留餘數法
在我國,居民身份證號是由18位數字組成的,比如:110323198512166666。
哈希表充分體現了空間換時間的思想,如果我們真的有9999999999999999999個空間,那麼我們完全可以從下標爲0開始存放,當然這是不切實際的,而且如果真的這樣做,也是對空間的極大浪費。對於較大的整數,並且這個整數的每幾位還存在某些含義時,我們處理方法是模以一個素數。
下面給出了大整數在lwr-upr之間的最佳取模的素數:或者點擊這個鏈接–>最佳取模的素數
之所以模以一個素數,是因爲模以一個素數可以減少hash衝突並且能較爲充分地利用到大整數的每部分數據。
比如:
在模以4時產生了嚴重的Hash衝突,而模以素數7在這組數據中沒有發生Hash衝突。
- 特殊類型構建Hash函數
對於特殊類型的數據,我們依然是將其轉化爲整數:比如字符串
根據實際需求,我們也可以將字符串轉化成B進制的整數。那麼hash函數就是這樣的:
上面三個hash函數是等價的,只是在第一個hash函數下簡化了計算而已。
Hash衝突解決方法
由於具體問題的複雜性,Hash衝突不可避免的存在,因此就需要對Hash衝突進行處理,通常較好的方式是:鏈地址法。
例如通過Hash函數計算得到 k1的地址爲4,k2的地址爲1,分別插入後又來了一個k3且地址也是1,此時k1和k3發生了衝突,如何處理呢?
鏈地址法就是讓發生衝突的元素以鏈表插在前一個元素後面:
事實上發生衝突時並不一定要構成一個鏈表,只要是查找表就行,也就是說我們完全可以鏈接一個AVL樹或者紅黑樹。
在Java中HashMap就是TreeMap的數組;HashSet是TreeSet的數組。
基於TreeMap實現HashMap
package cn.boom.hash;
import java.util.Arrays;
import java.util.TreeMap;
public class HashTable<K, V> {
//取模的素數
private static int[] prime = {53,97,193,389,769,1543,3079,6151,12289,
24593,49157,98317,196613,393241,786433,
1572869,3145739,6291469,12582917,25165843,
50331653,100663319,201326611,402653189,805306457,1610612741};
private static final int upperTol = 10; //平均hash衝突上界
private static final int lowerTol = 2; //平均hash衝突下界
private TreeMap<K,V>[] hashTable;
private int capacity;
private int size;
private int capacityIndex;
public HashTable(){
this.size = 0;
this.capacityIndex = 0;
this.capacity = prime[capacityIndex];
this.hashTable = new TreeMap[capacity];
for (int i = 0; i < capacity; i++) {
hashTable[i] = new TreeMap<K, V>();
}
}
public int getSize() {
return size;
}
public boolean contains(K key) {
int address = hash(key);
return hashTable[address].containsKey(key);
}
//hash函數
private int hash(K key) {
return key.hashCode() & 0x7fffffff % capacity;//取key hashCode的正值並計算hash值
}
private void resize(int newCapacity){
TreeMap<K, V>[] newHashTable = new TreeMap[newCapacity];
for(int i = 0 ; i < newCapacity ; i ++)
newHashTable[i] = new TreeMap<K,V>();
int oldCapacity = this.capacity;
this.capacity = newCapacity;
for(int i = 0 ; i < oldCapacity ; i ++)
for(K key: hashTable[i].keySet())
newHashTable[hash(key)].put(key, hashTable[i].get(key));
this.hashTable = newHashTable;
}
public void add(K key, V value) {
int address = hash(key);
if (hashTable[address].containsKey(key)) {
hashTable[address].put(key, value);
} else {
hashTable[address].put(key, value);
this.size++;
if (this.size >= this.capacity * upperTol && capacity+1 < prime.length) {
resize(prime[(capacityIndex++)]);
}
}
}
public V remove(K key) {
int address = hash(key);
if (!hashTable[address].containsKey(key)) {
throw new IllegalArgumentException(key + " doesn't exist ! ");
}
V ret = hashTable[address].remove(key);
size--;
if (this.size < this.capacity * lowerTol && capacityIndex - 1 >= 0) {
resize(prime[(capacityIndex--)]);
}
return ret;
}
@Override
public String toString() {
return "HashTable{" +
"hashTable=" + Arrays.toString(hashTable) +
", capacity=" + capacity +
", size=" + size +
", capacityIndex=" + capacityIndex +
'}';
}
}