數據結構之哈希表
在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1)
-
數據結構的物理存儲結構只有兩種:
- 順序存儲結構
- 鏈式存儲結構
哈希表的主幹就是數組。對於數組通過指定下標的查找,時間複雜度爲O(1)
-
查找的本質:存儲位置 = f(關鍵字),f是一個哈希函數
哈希衝突:但是,鍵是可能存在衝突的,相當於不同的鍵得出了相同的哈希值。HashMap即是採用了鏈地址法,也就是數組+鏈表的方式。
HashMap的結構
- HashMap的主幹是一個Entry數組。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
- Entry是HashMap中的一個靜態內部類,它實現了一個鏈表結構。鏈表則是主要爲了解決哈希衝突而存在的。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算
...
}
-
整體結構如下
- 解決的問題:哈希衝突
- 如果定位到的數組位置不含鏈表,即當前entry的next指向null,則一次查詢即可。
- 如果定位到的數組包含鏈表
- 添加:O(1),直接插入鏈表頭部
- 查找:O(n),遍歷鏈表,key對象的equals方法逐一比對查找
HashMap的源碼分析:插入
public V put(K key, V value) {
//其允許存放null的key和null的value,放在table[0]
if (key == null)
return putForNullKey(value);
int hash = hash(key);
//得到鍵的哈希值,用來獲取數組中的索引
int i = indexFor(hash, table.length);
//如果i處的Entry不爲null,則需要在鏈表中添加,但是添加前需要看是否已存在,存在返回舊值,不存在則最終addEntry。
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);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//添加前看是否需要擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
// 獲取指定 bucketIndex 索引處的 Entry
Entry<K,V> e = table[bucketIndex];
// 將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entr
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
HashMap的源碼分析:讀取
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
//通過哈希得到的index的e不爲空則繼續搜索鏈表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
HashMap的性能參數
-
initialCapacity
初始容量 -
transient int size;
實際存儲的key-value鍵值對的個數 -
int threshold;
最大容量,threshold一般爲 capacity*loadFactory,HashMap在進行擴容時需要參考threshold。初始容量默認爲16 -
final float loadFactor;
負載因子,代表了table的填充度有多少,默認是0.75,因此如果負載因子越大,對空間的利用更充分,然而後果是查找效率的降低;如果負載因子太小,那麼散列表的數據將過於稀疏,對空間造成嚴重浪費。 -
transient int modCount;
用於防止多線程問題的快速失敗。由於HashMap非線程安全,在對HashMap進行迭代時,如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經有其他線程修改了Map,則需要拋出異常ConcurrentModificationException
HashMap的擴容
- HashMap數組的大小需要擴容時,原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。
- loadFactor的默認值爲0.75。默認情況下,數組大小爲16,那麼當HashMap中元素個數超過160.75=12的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置。
- 這是一個非常消耗性能的操作,可以預設一個大小。
參考
- Java集合學習1:HashMap的實現原理,李大輝,http://tracylihui.github.io/2015/07/01/Java%E9%9B%86%E5%90%88%E5%AD%A6%E4%B9%A01%EF%BC%9AHashMap%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/
- HashMap實現原理及源碼分析,dreamcatcher-cx,http://www.cnblogs.com/chengxiao/p/6059914.html
關於我:
linxinzhe,全棧工程師,目前供職於某世界500強銀行的金融科技部門(人工智能,區塊鏈)。
GitHub:https://github.com/linxinzhe
歡迎留言討論,也歡迎關注我~
我也會關注你的哦!