關於哈希表(Hashtable)個人學習理解

數據結構–哈希表(Hashtable、又稱散列表)


最近做了一個題目:想要查看集合中的某個指定元素,但是不知道具體的位置。
一般情況下是遍歷這個數組的全部,然後去找到這個元素。若此時元素基數不是很大的話,還好,若集合的長度 n 趨於無窮大,而恰恰不巧的是這個元素的位置剛好在最後,那麼可想而知消耗的時間是非常巨大的。那麼就需要一個高效的存儲結構來存儲這個集合----哈希表。使用了這種結構能夠提高查找元素的速度,但是卻不能保證元素的順序。

1. 基本概念

在學習哈希表之前我們需要先明白幾個基本概念:

  1. 散列碼
    在定義一個對象的時候可以通過重寫hasCode() 方法來爲不同對象生成一個獨一無二的數值字符串,這個數值就是散列碼;這個散列碼一般情況下是不會相同的(但是也存在例外,這裏會在後面說明)
  2. 鍵(key)
    哈希表是通過一個標記來尋找與這個標記相對應的值的。這個標記與值之間形成一種映射關係。
  3. 值(value)
    即需要獲得的信息。
  4. 哈希函數
    散列表是將數據存放在散列表中的,那麼一個數據在散列表彙總具體存放的位置就是由哈希函數來進行決定的,通過對 key 值的 散列碼進行 哈希函數的計算,得出一個具體的地址(數值),然後將需要存放的數據放入這個地址中。
  5. 哈希地址
    這個地址記錄的就是在哈希表中存放數據的的位置,但是:哈希地址只不過是相對於哈希表來說的一個地址,並不是真實的對應於一個屋裏存儲地址。意思是這個哈希地址只適用於相對應的哈希表中。(而且不同的哈希表可能採用的哈希算法也不同,得到的地址也不同,所以更別說適用了)。

前面幾者之間的關係

哈希表是通過 哈希函數 來構建的,哈希函數類似於 address = f(x) address就是哈希地址,x 就是 key 值的散列碼,不同的key值有不同的散列碼,而 address 地址指向的就是散列表中的一個存儲空間,這個存儲空間裏用來存放 value ,這個 value 和 相應的 key 之間形成了一種映射。

2. 哈希衝突

有的時候因爲沒有選取合適的 哈希函數 f(x) ,或者出現了相同的散列碼,導致經過計算後的 哈希地址(address)出現了重複的現象。那麼就會與之前位置上已經存放好的數據產生衝突。這就叫哈希衝突。
哈希衝突只能儘量減少,不能完全能避免

3. 常見的哈希函數

常見的方法有6種:

  1. 直接定製法
    一次函數,或者直接取值。也叫自身函數;
    例如:有一個從1到100歲的人口數字統計表,其中,年齡作爲關鍵字,哈希函數取關鍵字自身。

  2. 數字分析法
    如果關鍵字由多位字符或者數字組成,就可以考慮抽取其中的 2 位或者多位作爲該關鍵字對應的哈希地址,在取法上儘量選擇變化較多的位,避免衝突發生。
    例如:

  3. 平方取中法
    平方取中法是對關鍵字做平方操作,取中間幾位作爲哈希地址(此方法是比較常用的構造哈希函數的方法)
    例如關鍵字序列爲 {421,423,436},對各個關鍵字進行平方後的結果爲{177241,178929,190096},我們則可以取中間的兩位{72,89,00}作爲其哈希地址。

  4. 摺疊法
    將關鍵字分割成位數相同的幾部分(最後一部分的位數可以不同),然後取這幾部分的疊加和(捨去進位)作爲哈希地址,這方法稱爲摺疊法。
    例如:每一種西文圖書都有一個國際標準圖書編號,它是一個10位的十進制數字,若要以它作關鍵字建立一個哈希表,當館藏書種類不到10,000時,可採用此法構造一個四位數的哈希函數。

  5. 除留餘數法
    取關鍵字被某個不大於哈希表表長m的數p除後所得餘數爲哈希地址。即:
    address = f(key) = x % p ( p <= m )

  6. 隨機數法
    選擇一個隨機函數,取關鍵字的隨機函數值爲它的哈希地址,即
    f(key)=random(key),其中random爲隨機函數。通常用於關鍵字長度不等時採用此法。

    注意:這個隨機函數是一個僞隨機函數 , 一般的隨機函數每次生成的值都是不一樣的,但是在這裏並不是的,這裏針對同一個 key 值生成的隨機數都是一樣的,但是不同的 key 值生成的 隨機數是不同的。

4. 解決哈希衝突

衝突:在哈希表中,不同的關鍵字值對應到同一個存儲位置的現象。即關鍵字 key1 ≠ key2 ,但 f ( key1 ) = f ( key2 ) 。均勻的哈希函數可以減少衝突,但不能避免衝突。發生衝突後,必須解決;也即必須尋找下一個可用地址。
無論哈希函數設計有多麼精細,都會產生衝突現象,也就是2個關鍵字處理函數的結果映射在了同一位置上,因此,有一些方法可以避免衝突。

具體的解決方案可以看我的另一篇博客:哈希衝突的解決方案

5. 哈希表的具體實現

話不多說,咱們直接看源碼:

    /**
     * Constructs a new, empty hashtable with the specified initial
     * capacity and the specified load factor.
     *
     * @param      initialCapacity   the initial capacity of the hashtable.
     * @param      loadFactor        the load factor of the hashtable.
     * @exception  IllegalArgumentException  if the initial capacity is less
     *             than zero, or if the load factor is nonpositive.
     */
    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

在這個構造函數中,前面幾行是用來處理錯誤的機制,注意 table = new Entry[initialCapacity] 可以看出來Hashtable的底層實現使用的 Entry 類數組。

下面看一下這個 Entry 是一個什麼樣的東東:

這個Entry 類是 HashTable類中的一個內部類,通過Entry 可以構成一個單向的鏈表,這也就能理解哈希表的底層是一個數組+鏈表實現的。(HashTable的具體實現示意圖文末可見)

那麼我們再來看看在向哈希表中添加一個元素時候是怎麼實現的。

① HashTable中不允許插入空值。當value = null 時候拋出異常。

② 散列碼就是針對 key 值的HashCode,index是哈希地址;而且可以看出使用的哈希函數是除留餘數法。

③ 將原先哈希表中的 對應 index 的值取出來,如果當前的哈希地址中已經存在數據,那麼久進入循環體,進行判斷,是否完全覆蓋掉之前的數據,若覆蓋掉之前的數據,則返回之前數據,併成功覆蓋。(判斷兩個數據一樣的條件是散列碼hash一樣,然後key值也要相等)

④ 將當前滿足的數據 put 入哈希表中。

下面再針對 第④ 步的 addEntry 進行進一步說明:

threshold 是一個數量標誌,當哈希表中的數據大於或等於這個標誌的時候就進行二次哈希。這裏插入一下標誌位的定義:

在一開始的構造器中對標誌位 threshold 進行賦值,(標誌位大小 = 哈希表容量 * 負載因子,即這個標誌位就是哈希表中正常存放數據的數量,一旦大於這個數量就觸發二次哈希,一般負載因子的默認值爲 0.75 ,即一個 initialCapacity 爲 16的哈希表,當存儲數據量達到 16 x 0.75 = 12 的時候就觸發二次哈希機制。 )

② 否則就新建一個 Entry 對象,然後將當前要put 進的元素放入 當前哈希地址所指向的位置。再count++。

剛纔說到二次哈希,那麼現在再看看二次哈希具體是怎麼實現的。


① 可以看出來每次擴容的大小是乘以 2 的 次冪個 然後再 +1 ,即在當前容量的基礎上翻一倍 再 +1 。

② 對之前的 哈希表中的數據進行遍歷並且二次哈希,之後放入新的哈希表中。
一般情況下,哈希表的初始容量大小是 11 ,負載因子大小是 0.75; 但是HashMap的初始容量大小是 16,這裏我也有些不是很明白,爲什麼要定義爲 11.希望有人能夠給我一個解答。

剛說完HashTable是怎麼實現的以及如何put元素,那麼不得不說一說如何get 元素;

給出參數 key ,對 key值 進行哈希運算,得到相應的哈希地址,然後判斷是否有相同的哈希地址的數據,若存在,則開始遍歷相同地址元素後面的鏈表。直到找到需要的元素。(判斷元素一致的規則前面已經說過了)。

實現示意圖

有些寫的不完善的地方還請指正。感謝賜教!

參考資料

jdk8 源碼
Java核心技術(卷一)
百度百科
csdn博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章