淺談哈希表與其映射函數(哈希函數)

哈希表又稱散列表,通過把關鍵字key映射到數組中的一個位置來訪問記錄。映射過程通過函數實現,而這個函數就叫哈希函數,存放關鍵字的數組稱爲散列表。

哈希表結構

這裏寫圖片描述

前面說了,關鍵字是存放在數組中的,所以哈希表的結構其實就是一個數組,爲什麼要採用數組來作爲哈希表的數據結構呢?這裏我不得不說數組的一些特性。
數組的時間複雜度是O(1),這裏說的時間複雜度是訪問複雜度,不是遍歷複雜度。計算機內存被設定爲直接訪問任意一個地址的時間是一致的,結合該特性,我們知道數組在內存中的存儲是,數組名存放在棧內存中,保存數組第一個元素的地址,而數組本身存放在堆內存中,其存儲空間是連續的,所以我們只需要知道數組名就可以非常快速的定位數組任意位置。

哈希算法

哈希算法的作用是將關鍵字通過一系列計算,得出的結果作爲數組下標,然後再將關鍵字存放到該下標對應的位置。哈希算法是決定哈希表中元素排列結構的最主要因素,不同的哈希算法會導致同樣的數據出現不同的存儲順序。針對不同類型的關鍵字哈希算法也不同。下面是針對整數關鍵字的幾種哈希算法(如果關鍵字是字符串,也可將字符串所有字符的ASCII碼加起來得到一個整數再進行計算):
直接取餘:顧名思義,直接取餘法是用關鍵字除以一個固定值取餘數,一般這個固定值取哈希表的大小。如果一個哈希表的大小是16,那麼關鍵字100的存放位置應該是數組下標爲4的位置。

乘積取整:成績取整法是用關鍵字乘以一個常數A(0<A<1),然後取乘積的小數部分再與哈希表的大小求積,最後取結果的整數部分。同上,若哈希表的大小爲16,A取0.025,那麼關鍵字100的存放位置應該是數組下標爲8的位置。
上面提到的兩種算法都非常簡單,但都存在很大的問題,這兩種算法在某種程度上都會產生大量的衝突,即不同的數通過算法得出的結果相同,這樣會令哈希的效果大打折扣。下面介紹一種解決衝突非常有效的哈希算法,經典哈希算法Time33

uint32_t time33(char const *str, int len) 
    { 
        unsigned long  hash = 0; 
        for (int i = 0; i < len; i++) { 
            hash = hash *33 + (unsigned long) str[i]; 
        } 
        return (hash & 0x7FFFFFFF); 
    }

從代碼中我們可以看出,這種算法就是將關鍵字每一位拿出來,逐個相加,每相加一次都乘以33,最終獲得我們要的結果。該算法可最大程度防止衝突的發生,亦可避免字符串類型的關鍵字順序不同而所含字符相同導致計算結果相同(如adcfg和dcfga)。在php中,一個字符串長度過長不好計算,我們可以將其先利用MD5加密轉化爲32位的字符串,然後再對這個字符串進行計算即可。

在學習Time33之前,我自己也想過一個避免該類問題的算法,供大家討論,思想如下:
這裏寫圖片描述
如圖,針對一個關鍵字,我們可對其從1開始進行依次編號,然後取每一位的值或者ASCII碼值與它所對應的編號相乘,最後求和。這種方法與Times33類似,都可以有效的解決大量衝突問題,但是仍避免不了一些及特殊的情況。爲了防止這些哈希算法無法避免的衝突,所以人們開始從哈希表的結構下手,希望通過改變哈希表的結構來避免這些衝突,因此又出現了許多避免衝突的方法,如“拉鍊法”解決衝突。

“拉鍊法”解決衝突的做法是將所有哈希值相同的關鍵字節點鏈接在同一個鏈表中。如下圖:

這裏寫圖片描述

“拉鍊法”將哈希值相同的關鍵字通過鏈表的方式連接起來,確實有效的解決了衝突問題,但是在查詢關鍵字的時候,若所查詢的位置無衝突,那麼查詢的時間複雜度爲O(1),但如果所查詢的位置出現衝突,就需要進一步遍歷鏈表去查詢,這樣的話查詢的效率大打折扣,時間複雜度並不能滿足所謂的O(1),所以說哈希表只能在理想無衝突的情況下,時間複雜度才能到達O(1)。因此來看,要降低時間複雜度最終的瓶頸還是在怎麼防止衝突的問題上而不是出現衝突怎樣處理的問題上。但是並不產生衝突幾乎是不可能實現的,那麼有沒有一種辦法既能有效處理出現的衝突,又能優化時間複雜度呢?我不知道現在有沒有這種方法,但是我提供一種思路供大家思考,這種思路叫“二次散列”。

“二次散列”,說白了就是對每一個關鍵字都進行兩種不同形式/函數的散列,然後根據兩次的結果確定該關鍵字的位置(如果兩個不同的關鍵字通過哈希函數A算出的哈希值相等,那麼他們通過哈希函數B所算出的哈希值幾乎不可能相等,哈希函數A和B可自行決定,但是最好取兩種散列思路不同的算法)。而存儲方式就叫“座標法”,跟”拉鍊法”大體相似,不同的是,”拉鍊法”採用鏈表儲存衝突,“座標法”利用數組存儲衝突。該結構的特點是它有一個縱向數組,類似於哈希表的原始結構,縱向數組每一位對應一個橫向數組(橫向數組命名應爲編號式,下劃線後的數字爲所對應的縱向數組下標,如M_0[]、M_1[]….),第一次散列所計算出哈希值爲縱向數組下標,若該位置爲空說明無衝突發生,直接存儲;若有值,判斷是否相等,若不等說明衝突,先爲該下標申請其所對應的橫向數組,然後進行第二次散列,第二次散列所對應的哈希值爲橫向數組的下標,也就是該關鍵字所要存放的位置,類似於X/Y軸,查詢的時候最多隻需做兩次散列一次比較,就可以確定關鍵字的具體位置。爲什麼不直接使用二維數組呢?因爲畢竟不是所有位置都會發生,使用二維數組會過度消耗內存空間。該方法還是存在很多缺陷,只提供一個思考的方向。

總的來說,散列函數的好壞是決定哈希表性能的關鍵,一個好的哈希函數一定具備兩個特性,一是足夠聚集,二是不重疊,這樣即保證了內存的有效利用也保證了查詢的時效,但這兩個特性在理論上相悖,只能去找一個平衡點,使性能最大化。

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