談談面試--雜湊表系列
前言:
我以前在百度的mentor, 在面試時特喜歡考察雜湊表. 那時的我滿是疑惑和不解, 覺得這東西很基礎, 不就的分桶理念(以空間換時間)和雜湊函式選擇嗎? 最多再考察點衝突解決方案. 為何不考察類似跳躍表, LSM樹等高階資料結構呢?
隨著工程實踐的積累, 慢慢發現了自己當初的膚淺. 面試的切入點, 最好是大家所熟悉的, 但又能從中深度挖掘/剖析和具有區分度的.
本文結合自己的工程實踐, 來談談對雜湊表的優化和實踐的一些理解.
基礎篇:
雜湊表由一定大小的連續桶(bucket)構成, 藉助雜湊函式對映到具體某個桶上. 當多個key/value對聚集到同一桶時, 會演化構成一個連結串列.
雜湊表結構有兩個重要的引數, 容量大小(Capacity)和負載因子(LoadFactor). 兩者的乘積 Capacity * LoadFactor決定了雜湊表rehash的觸發條件.
以空間換時間為核心思想, 確保其資料結構的訪問時間控制在O(1).
雜湊表隱藏了內部細節, 而對外的使用則非常的簡單. 只需定義key的hash函式和compare函式即可.
以Java為例, 其把預設的hash函式和equals函式置於頂層的Object基類中.
1
2
3
4
5
6
|
class Object
{ public native int hashCode(); public boolean equals(Object
obj) { return ( this ==
obj); } } |
所有的子類, 需要過載hashCode和equals就能方便的使用雜湊表.
進階篇:
hash函式的選擇需保證一定雜湊度, 這樣才能充分利用空間. 事實上雜湊表的使用者, 往往關注hash函式的快速計算和高雜湊度, 卻忽視了其潛在的風險和危機.
1). hash碰撞攻擊
前段時間, php爆出hash碰撞的攻擊漏洞. 其攻擊原理, 簡單可概括為: 特定的大量key組合, 讓雜湊表退化為連結串列訪問, 進而拖慢處理速度, 請求堆積, 最終演變為拒絕服務狀態.
具體可參考博文: PHP雜湊表碰撞攻擊原理.
大致的思路是利用php雜湊表大小為2的冪次, 索引位置計算由 hash(key) % size(bucket) 轉變為 hash(key) & (1^n - 1).
黑客(hacker)知曉time33演算法和hash函式, 可以構造/收集特定的key系列, 使得其hash(key)為同一桶索引值. 通過post請求附帶, 導致php構造超長鏈的雜湊表.
其實如果能理解hash碰撞攻擊的原理, 說明其對hash的衝突處理和雜湊表本身的資料結構模型有了較深的理解了.
2). 分段鎖機制
如果加鎖是不可避免的選擇, 那能否減少鎖衝突的概率呢?
答案是肯定的, 不同桶之間的key/value操作彼此互不影響. 在此前提下, 對雜湊桶進行分段加鎖. 這樣全域性鎖就退化為多個分段鎖, 而鎖衝突的概率由於分割槽的原因, 降低至1/N (N為分段鎖個數).
Java併發類中的ConcurrentHashMap也是採用類似的思想來實現, 不過比這複雜多了.
難度篇:
雜湊表單key的操作複雜度為O(1), 效能異常優異. 但需要對雜湊表進行迭代遍歷其所有元素時, 其效能就非常的差. 究其原因是各個key/value對分散在各個桶中, 彼此並無關聯. 元素遍歷轉化為對雜湊桶的全掃描.
那如果存在這樣的需求, 既要保證O(1)的單key操作時間複雜度, 又要讓迭代遍歷的複雜度為O(n) (n為雜湊表的key/value對個數, 不是桶個數), 那如何去實現呢?
1). LinkedHashMap&LRU快取
是否存在一個複合資料結構, 既有Hashmap的特性, 又具備DoubleLinkedList線性遍歷的特徵?
答案是肯定的, 該複合結構就是LinkedHashmap.
注: 依次新增key1, key2, ..., key6, 其按插入順序構成一個雙向列表.
一圖勝千言, 該圖很形象的描述了LinkedHashMap的構成. 可以這麼認為: 每個hash entry的結構的基礎上, 新增prev和next成員指標用於維護雙向列表. 實現就這麼簡單.
在工程實踐中, 往往採用LinkedHashMap的變體來實現帶LRU機制的Cache.
簡單描述其操作流程:
(1). 查詢/新增key, 則把該key/value對擱置於LRU佇列的末尾
(2). 若key/value對個數超過閾值時, 則選擇把LRU佇列的首元素淘汰掉.
模擬key5元素被查詢訪問, 成為最近的熱點, 則內部的連結模型狀態轉變如下:
注: key5被訪問後, 內部雙向佇列發生變動, 可以理解為刪除key5, 然後再新增key5至末尾.
JAVA實現帶LRU機制的Cache非常的簡單, 用如下程式碼片段描述下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class LRULinkedHashMap<K,
V> extends LinkedHashMap<K,
V> { private int capacity
= 1024 ; public LRULinkedHashMap( int initialCapacity, float loadFactor, int lruCapacity)
{ //
access order=> true:訪問順序, false:插入順序 super (initialCapacity,
loadFactor, true ); this .capacity
= lruCapacity; } @Override protected boolean removeEldestEntry(Entry<K,
V> eldest) { if (size()
> capacity) { return true ; } return false ; } } |
注: 需要注意access order為true, 表示按訪問順序維護. 使用Java程式設計的孩子真幸福.
當雜湊表中的元素數量超過預定的閾值時, 就會觸發rehash過程. 但是若此時的hash表已然很大, rehash的完整過程會阻塞服務很長時間. 這對高可用高響應的服務是不可想象的災難.
面對這種情況, 要麼避免大資料量的rehash出現, 預先對資料規模進行有效評估. 要麼就繼續優化雜湊的rehash過程.
2). 0/1切換和漸進式rehash
redis的設計者給出了一個很好的解決方案, 就是0/1切換hash表+漸進式rehash.
其漸進的rehash把整個遷移過程拆分為多個細粒度的子過程, 同時0/1切換的hash表共存.
redis的rehash過程分兩種方式:
• lazy rehashing: 在對dict操作的時候附帶執行一個slot的rehash
• active rehashing:定時做個小時間片的rehash
相關文章
- 淺談雜湊表
- 幾道和雜湊(雜湊)表有關的面試題面試題
- 雜湊表(雜湊表)詳解
- 淺談演算法和資料結構(11):雜湊表演算法資料結構
- 面試必談的雜湊,.Net 程式設計師溫故而知新面試程式設計師
- 雜湊表(雜湊表)原理詳解
- 雜湊表
- 【尋跡#3】 雜湊與雜湊表
- 字串雜湊表字串
- 6.7雜湊表
- 查詢(3)--雜湊表(雜湊查詢)
- 淺談雜湊法及其解決衝突的方法
- 淺談最長迴文子串求法——字串雜湊字串
- 談談面試與面試題面試題
- 深入理解雜湊表(JAVA和Redis雜湊表實現)JavaRedis
- 雜湊表應用
- 雜湊表的原理
- 實現雜湊表
- Swift雜談Swift
- synchronized雜談synchronized
- IT者雜談
- fragment雜談Fragment
- 退役雜談
- 符號表與雜湊表符號
- 雜談 —— 面試有感+今日摸魚學習HashMap面試HashMap
- R語言——雜湊表R語言
- JAVA 實現 - 雜湊表Java
- 【面試普通人VS高手系列】談談你對Seata的理解面試
- 演算法面試通關40講 - 雜湊表/對映演算法面試
- 【閱讀筆記:雜湊表】Javascript任何物件都是一個雜湊表(hash表)!筆記JavaScript物件
- CodeReview雜談View
- 【雜談】策略模式模式
- 資料雜談
- 雜談 CSS IN JSCSSJS
- 雜談其一
- 免殺雜談
- 數學雜談 #??
- 正則雜談