文章目錄
- 1. 如何設計散列函數
-
- 1.1 散列函數設計不能太複雜
- 1.2 散列函數生成的值要盡可能随機并且均勻分布
- 2.裝載因子過大怎麼辦
- 3.如何避免低效地擴容
- 4.如何選擇沖突解決方法
- 5.工業級散清單的特性&設計
-
- 5.1 Java HashMap工業級連結清單實作
-
- 5.1.1 初始大小
- 5.1.2 裝載因子和動态擴容
- 5.1.3 散列沖突解決方法
- 5.1.4 散列行數
- 5.2 工業級散清單應該由哪些特性?
- 5.3 如何實作?
如何打造一個工業級水準的散清單?如何設計一個可以應對各種異常情況的工業級散清單,來避免在散列沖突的情況下,散清單性能的急劇下降,并且能抵抗散列碰撞攻擊?
1. 如何設計散列函數
1.1 散列函數設計不能太複雜
過于複雜的散列函數,勢必會消耗很多的計算時間們也就間接地影響散清單的性能。
1.2 散列函數生成的值要盡可能随機并且均勻分布
2.裝載因子過大怎麼辦
裝載因子越大,說明清單中的元素越多,空閑位置就越少,散列沖突的機率就越大,不僅插入資料的過程要多次尋址或者拉很長的鍊,查找的過程也會是以變得很慢。
插入一個資料,最好情況下,不需要擴容,最好時間複雜度為O(1).
最壞的情況,散清單裝載因子過高,重新擴容,我們需要重新申請存儲空間,涉及到三方面:
- 重新擴容
- 重新計算哈希位置
-
搬移資料
,時間複雜度為O(1), 用攤還分析法,時間複雜度為O(1).
裝載因子閥值需要選擇得當。如果太大,會導緻沖突過多;如果太小,會導緻記憶體浪費嚴重。需要權衡時空複雜度:
- 如果空間緊張,對執行效率要求又不高,可以增加負載因子,設定可以大于1.
- 反之,對執行效率要求很高,對記憶體空間不敏感,則可以降低負載因子.
3.如何避免低效地擴容
大部分情況下,動态擴容的散清單插入一個資料都很快,但是在特殊情況下,當裝載因子已經達到門檻值,需要先進行擴容,再插入資料。這個時候,插入資料就會變得很慢, 甚至會無法接受。
舉一個極端例子,當散清單目前大小為1GB, 要想擴容為原來的兩倍大小,那麼就需要對1GB的資料重新計算哈希值,并且從原來的散清單版移到新的散清單,很耗時。
為了解決這個問題,可以将擴容操作穿插在插入操作的過程中,分批完成。當裝載因子觸發門檻值後,我們隻重新申請空間,但并不将老的資料搬移到新的散清單中。
當有新的資料插入時,首先将新的資料插入新的散清單中,并且從老的散清單中拿出一個資料放入新散清單。每插入一個資料到散清單,我們都重發上面的過程。經過多次插入操作後,老的散清單中的資料就一點一點全部搬遷至新的散清單中了。這樣沒有一次性資料搬移。插入操作就會變得很快。
對于這期間的查找,相容,先從新散清單中查找,沒有找到,再去老散清單中找。通過這樣将操作均攤到多次的方法,避免了一次性擴容耗時過多的情況,是以時間複雜度為O(1).
4.如何選擇沖突解決方法
-
開放尋址法
e.g. ThreadLocalMap
- 優點
- 資料全部在數組中,可以有效地利用CPU緩存加快查詢速度
- 序列化簡單,連結清單法包含指針,序列化比較困難
- 缺點
- 删除資料麻煩,需要特殊标記
- 沖突代價太高,是以裝載因子上限不能太大,這也導緻這種方法比連結清單法更浪費記憶體空間
- 适用場景
- 資料量小
- 裝載因子小
- 優點
-
連結清單發
e.g. LinkedHashMap
- 優點
- 連結清單結點可以在需要時再建立,并不需要像開放尋址那樣事先申請好,是以記憶體利用較開放尋址法高
- 對大裝載因子容忍度更高,隻要散列函數值均勻分布,即便裝載因子變成10,也就是連結清單的長度變長,雖然效率有所下降,但是比起順序查找還是快和很多
- 缺點
- 需要存儲指針,如果儲存資料本身不大,會導緻記憶體翻倍的情況出現
- 連結清單結點零散分布,對CPU緩存不友好,對執行效率也有一定影響
- 适用場景
- 存儲大對象、大資料量
- 靈活,可以支援多種優化政策,比如用紅黑樹代替連結清單
- 優點
5.工業級散清單的特性&設計
5.1 Java HashMap工業級連結清單實作
5.1.1 初始大小
預設16,可調整。如果大機率知道數量有多大,可以修改預設值,避免resize數量,這樣會大大提高性能.
5.1.2 裝載因子和動态擴容
預設裝載因子為0.75, 達到門檻值會擴容為原來2倍。
5.1.3 散列沖突解決方法
使用連結清單法。Jdk1.8中,進行了進一步優化。當連結清單長度太長(預設超過8), 連結清單就轉為紅黑樹. 可以利用紅黑樹快速增删改查的特點,提高HashMap的性能。當紅黑樹節點小于8個時,又退化為連結清單。因為在資料量較小的情況下,紅黑樹要維護平衡,比起連結清單,性能上優勢并不明顯。
5.1.4 散列行數
設計并不複雜,最求的是簡單高效,分布均勻。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
5.2 工業級散清單應該由哪些特性?
- 支援快速查詢,插入,删除操作
- 記憶體占用合理,不能浪費太多的記憶體空間
- 性能穩定,極端情況下,散清單的成本效益也不會退化到無法接受的情況
5.3 如何實作?
可以從下面三個方面來考慮設計思路:
- 設計一個合适的散列函數
- 定義裝載因子門檻值,并設計動态擴容政策
- 選擇合适的散列沖突解決方法