衆所周知,HashMap是基於has表實現是的Map。那麼,現在,我們首先來分析下什麼交hash表。
1.首先我們來看下哈希表的作用以及它的基本概念
我們平時查找數據可能會用到折半查找、二叉排序樹查找‘或者是B-樹查找,在查找數據時進行=、>、<的比較,所以查找的效率會依賴於查找過程中進行的比較次數。
我們理想的情況是不經過任何比較,一次存取便能得到所查記錄。這就要在記錄的儲存位置和它的關鍵字之間建立一個確定的對應關係f,使每個關鍵字和結構中一個惟一的存儲位置相對應。這個對應關係我們就稱之爲哈希函數,根據關鍵字key和f找到數據的存儲位置。對於不同的關鍵字,可能經過哈希函數的映射後會得到同一個值,即key1!=key2 , f(key1)= f(key2) ,這就得不到惟一的存儲位置了。對於這種情況,我們稱之爲衝突。在一般情況下衝突只能儘可能的少,而不能完全避免。
對於哈希表來說,還有一箇中重要的概念,即哈希表的填充因子:a=表中填入的記錄數/哈希表的長度。a標誌哈希表的裝滿程度。直觀的看,a越小,發生衝突的可能就越小;反之,a越大,表中已經填入的記錄越多,要再填入數據時,發生衝突的可能性就越大,則查找時,給定值需要與之進行比較的關鍵字的個數也就也多。
2。哈希表的構造
對於哈希表來說,它主要由三部分構成:哈希函數、哈希表、衝突處理
(1)哈希函數的構造方法:
1)直接定址法
取關鍵字或關鍵字的某個線性函數值爲哈希地址。即:
H(key)=key或H(key)a*key+b
其中a和b爲常數
例如:有一個從1歲到100歲的人口數字統計表,其中,年齡作爲關鍵字,哈希函數取關鍵字自身,如下表:
由於直接定址所得地址集合和關鍵字集合的大小相同,因此,對於不同的關鍵字不會發生衝突。但是,實際中使用這種方法的情況很少,因爲隨着關鍵字的增多,哈希表會變得很龐大。
2)平方去中位法:
取關鍵字平方後的中間幾位爲哈希地址。取的位數由表長決定。例子:
3)還有摺疊法、數字分析法、除留餘數法、隨機數法等
(2)處理衝突的方法
這裏只介紹兩種方法:
1)開放定址法
其中i=1,2,3。。。。,k(k<=m-1),H(key)爲哈希函數,m爲哈希表表長,di爲增量序列,可能有下列三種情況:di=1,2,3....,m-1,稱線性探測在散列;(2)
稱二次探測再散列;(3)di=僞隨機數序列,稱僞隨機探測再散列。
例如,在長度爲11的哈希表中已填有關鍵字分別爲17,60,29的記錄,(哈希函數H(key)=key MOD 11),現在有第四個記錄,其關鍵字爲38,由哈希函數得到哈希地址爲5,產生衝突。若用線性探測再散列的方法處理,得到下個地址是6,仍衝突,再求下個地址7,仍衝突,直到哈希地址爲8的 位置爲“空”時止,處理衝突的過程結束,記錄填入哈希表中序號爲8的位置。若用二次探測再散列,則應該填入序號爲4的位置。類似的可以得到僞隨機再散列的地址。如下圖
(a)插入前(b)線性探測再散列(c)二次探測再散列(d)僞隨機探測再散列,僞隨機數是9
2)鏈地址法
將所有關鍵字爲同義詞的記錄存儲在同一線性表中。假設某哈希函數產生的哈希地址在區間[0,m-1]上,則設立一個指針型向量
Chain Chain Hash[m];
其每個分量的初始狀態都是空指針。凡是哈希地址爲i的記錄都插入到頭指針爲ChainHash[i]的鏈表中。在列表中的插入位置可以在表頭或表尾;也可以在中間,以保持同義詞在同一線性表中按關鍵字有序。
例如:已知一組關鍵字爲(19,14,23,01,68,20,84,27,55,11,10,79),則按哈希函數H(key)=key MOD 13 和鏈地址法處理衝突構造所得的哈希表,如下圖所示:
———————————————————————————————————————————————————————————
現在我們來解釋下java是如何實現HashMap的
1.HashMap的哈希函數:
staticint hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
其中^是異或運算,>>>是無符號右移運算,則這個哈希函數主要是進行 的移位和異或運算。
staticint indexFor(int h, int length) {
return h & (length-1);
}
經過該函數得到哈希地址,其中&是對二進制的與運算
2.HashMap是用鏈地址法法來處理衝突
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
returnnull;
}
這段代碼是把一對映射數據存入HashMap中,如果存入的數據的鍵值存在,就返回鍵對應的值;如果不存在,就進行
addEntry(hash, key, value, i);操作,並且返回null,表示該數據還未在HashMap中。
值得說明下,輸入的KEY是一個類型如何變成整形數據呢,這裏的關鍵在: int hash = hash(key.hashCode());
hashCode是超類Object的方法,它能取到對象的內部地址並轉換成一個整數。
其中addEntry(hash, key, value, i)爲:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
而Entry<K,V>(hash, key, value, e);的代碼爲:
staticclass Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
finalint hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
由此我們可以看出,經過哈希函數映射後發生衝突的數據會存在Entry的列表中。
3.HashMap的哈希表
transient Entry[] table;
table就是我們上面提到的初始狀態都是空指針的表,它的大小爲:
staticfinalint MAXIMUM_CAPACITY = 1 << 30;
4.裝載因子
staticfinalfloat DEFAULT_LOAD_FACTOR = 0.75f;
當裝入table的數據超出上限(與裝載因子有關),則要重構table
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
--------------------------------------------------------------------------------------------------------------------
上述爲對hash表的簡單介紹以及HashMap的實現過程。
對hash表介紹參考嚴蔚敏老師 的《數據結構》