今天摸魚看了下HashMap
源碼,想起大神同窗面試遇到過面試官問Redis 字典
和HashMap
的哈希過程有何不一樣。。。老實說,也看過Redis設計與實現(真心推薦),可是準確地描述不出來,故寫下此文。java
注:因爲本文偏重於hash過程,源碼部分就看大神們淺顯易懂的敘述吧~面試
散列函數又稱散列算法、哈希函數,是一種從任何一種數據中建立小的數字「指紋」的方法,而這個「指紋」就是散列值。算法
散列函數的應用領域很廣,例如保護數據、確保傳遞真實的信息、散列表等。本文主要討論的是散列表上的應用。數組
固然咱們但願散列函數能保證每一個key對應一個「指紋」,也就是散列值,所謂的完美散列,但源於對性能、應用場景等考慮,能夠接受不太多的散列碰撞。bash
散列碰撞:散列函數的輸入和輸出不是惟一對應關係,例如散列函數的輸入A、B獲得的散列值都是C。數據結構
常見的散列函數:函數
直接定址法 數字分析法 平方取中法 摺疊法 除留餘數法 隨機數法性能
hash?->散列算法的選擇->散列衝突怎麼解決
想必這是大多數同行們的思惟定式了。那麼,咱們看下散列衝突的解決算法主要有什麼?LOAD_FACTOR
(負載因子)你們都知道HashMap
的默認LOAD_FACTOR
爲0.75
,它的做用是什麼呢?下面跟着源碼追尋蹤影把~優化
new
一個HashMap
,源碼註釋告訴咱們capacity
爲1六、load_factor
爲0.75。ui
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
複製代碼
put
元素,一定要進行擴容,看resize()
函數,指貼出標誌性的部分。else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
複製代碼
capacity
乘以load_factor
得出的,即16 * 0.75 = 12
。HashMap
中的元素多於閾值就要擴容,能夠理解爲閾值相對於容量小,就下降了散列衝突的概率,由於放入16個元素的散列衝突的概率,在相同的散列函數下,大機率比12個元素的散列衝突概率小。MOD
運算,而是用位運算,假設爲輸入值,散列函數爲具體公式以下:f(key) = hash(key) & (table.length - 1)
hash(key) = (h = key.hashCode()) ^ (h >>> 16)
複製代碼
hash(key) & (table.length - 1)
是對table.length
取餘的優化版,其實做用差很少,就是基於除留餘數法。因爲HashMap
的特性每次擴容table.length
都會是,所以位運算明顯效率更高。>>>
是無符號右移位運算符,咱們知道hashCode()
取值範圍很廣,自己衝突的可能性很小,可是與上table.length - 1
這個概率就變大了,由於table.length
是一個較小的值。這就是爲何會使用>>> 16
的緣由,hashCode()
的高位和低位都對f(key)
有了必定影響力,使得分佈更加均勻,散列衝突的概率就小了。HashMap
的散列衝突解決方法明顯是鏈地址法,其實從結構就能夠看出來。HashMap
的resize()
方法也能夠看出來,下面請看部分源碼。。。上的註釋。if (oldTab != null) {
// 遍歷舊數組上的節點
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 當前鏈表只有一個節點(沒有散列衝突的狀況)
if (e.next == null)
// 經過散列算法計算存放位置並放入
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 紅黑樹去了。。。
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 低位鏈表、高位鏈表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
// 遍歷發生散列衝突的鏈表
next = e.next;
// hash值小於舊數組容量 放入低位鏈表
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// hash值大於等於舊數組大小 放入高位鏈表
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 低位鏈表放在原來index下
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位鏈表放在原來index + 舊數組大小
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
複製代碼
HashMap
差很少的地方首先,簡單對Redis字典的數據結構進行簡要說明,都是我讀大學時產生陰影的C
語言。
typedef struct dictht {
// 哈希表數組
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩碼,用於計算索引值
// 老是等於size - 1
unsigned long sizemask;
// 哈希表已使用節點數
unsigned long used;
} dictht
複製代碼
Node<K,V>[]
殊途同歸之妙,接下來看看dictEntry
這個類。typedef struct dictEntry {
// 鍵
void *key
// 值
union {
void *val;
unit64_tu64;
int64_ts64;
} v
// next指針
struct dictEntry *next;
}
複製代碼
又有點眼熟了,鍵值對!key
屬性保存着鍵值對中的鍵,而v
屬性則保存着值,其中鍵值對的值能夠是一個指針、unit64_t
、int64_t
整數。next
是指向另外一個哈希表節點的指針,這個指針能夠將多個哈希表相同的鍵值對鏈接成一個鏈表。
能夠看到,目前爲止,HashMap
基本沒啥區別,除了多了一些額外屬性(哈希表大小、已使用節點數、掩碼等)。
typedef struct dict {
// 類型特定函數
dictType *type;
// 私有數據
void *privdata;
// 哈希表(上文講的)
dictht ht[2];
// rehash索引
// 當rehash不在進行時,值爲-1
int trehashidx;
}
複製代碼
hash = dict -> type->hashFunction(key)
ht[0]
或ht[1]
。其實和HashMap
的hash(key) & (table.length - 1)
同樣,由於註釋上說了sizemask
老是等於size - 1
。index = hash & dict->ht[x].sizemask
MurmurHash2
,筆者沒能力說明,給出鏈接。next
指針,多個節點造成單向鏈表,與HashMap
不一樣的是因爲沒有表尾指針,使用頭插法將新節點添加到鏈表的表頭位置。看到散列算法、散列衝突解決方式,沒有太大的區別,那麼差異到底在哪兒呢?那就是再哈希。
剛纔講到,字典數據結構裏有兩個哈希表(ht[2]
),祕密就在這裏。
Rehash
的目的在於爲了讓哈希表的負載因子維持在合理的範圍內,哈希表在鍵值對太多或者太少時,須要進行擴展或收縮。
步驟以下:
ht[1]
哈希表分配空間,若是執行的是擴展操做,那麼ht[1]
的大小爲第一個大於等於ht[0].used *2
的;若是執行的是收縮操做,那麼ht[1]
的大小第一個大於等於ht[0].used
的。ht[0]
中的全部鍵值對rehash
到ht[1]
上面,即從新計算鍵的哈希值和索引值,而後將鍵值對放置到ht[1]
哈希表的指定位置上。ht[0]
包含的全部鍵值對都遷移到了ht[1]
以後(ht[0]
變爲空哈希表),釋放ht[0]
,將ht[1]
設置爲ht[0]
,並在ht[1]
新建立一個空白哈希表,爲下一次rehash
作準備。注:漸進式Rehash本文沒講,畫圖筆者實在沒動力,數據結構較複雜,請諒解。
本文從散列算法、散列碰撞解決出發,簡要分析了HashMap
,Redis 字典
的hash
,觀點不必定都對,請各位大神批評指正!