談談面試--哈希表系列


前言:
  我之前在百度的mentor, 在面試時特喜歡考察哈希表. 那時的我盡是疑惑和不解, 以爲這東西很基礎, 不就的分桶理念(以空間換時間)和散列函數選擇嗎? 最多再考察點衝突解決方案. 爲什麼不考察相似跳躍表, LSM樹等高級數據結構呢?
  隨着工程實踐的積累, 慢慢發現了本身當初的膚淺. 面試的切入點, 最好是你們所熟悉的, 但又能從中深度挖掘/剖析和具備區分度的.
  本文結合本身的工程實踐, 來談談對哈希表的優化和實踐的一些理解.php

基礎篇:
  哈希表由必定大小的連續桶(bucket)構成, 藉助散列函數映射到具體某個桶上. 當多個key/value對彙集到同一桶時, 會演化構成一個鏈表.
  
  哈希表結構有兩個重要的參數, 容量大小(Capacity)和負載因子(LoadFactor). 二者的乘積 Capacity * LoadFactor決定了哈希表rehash的觸發條件.
  以空間換時間爲核心思想, 確保其數據結構的訪問時間控制在O(1).
  哈希表隱藏了內部細節, 而對外的使用則很是的簡單. 只需定義key的hash函數compare函數便可.
  以Java爲例, 其把默認的hash函數和equals函數置於頂層的Object基類中.java

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的衝突處理和哈希表自己的數據結構模型有了較深的理解了.redis

  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很是的簡單, 用以下代碼片斷描述下:編程

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數據結構

總結:
  哈希表做爲經常使用的數據結構, 被人所熟知. 但對其進一步的理解和挖掘, 須要真正的工程實踐積累. 洗盡鉛華始見真.併發

寫在最後:
  
若是你以爲這篇文章對你有幫助, 請小小打賞下. 其實我想試試, 看看寫博客可否給本身帶來一點小小的收益. 不管多少, 都是對樓主一種由衷的確定.ide

   

相關文章
相關標籤/搜索