哈希表的一些知識點

拋磚引玉,你們有沒有想過Object-C中,NSDictionary是如何實現根據key快速查值的?php

前言

在編程的世界中,比較重要的數據結構有如下3個:html

  • struct 結構體
  • array 數組
  • link list 鏈表

咱們都知道,數組是存儲數據的自然載體,在內存中是一段連續的地址,正是因爲這個特性,咱們可以經過數組的下標快速的獲取與之對應的value。本質上是經過下標來計算出value的內存地址。算法

NSDictionary自己是一種key-value的結構關係。其內部也是用數組來存儲value。那麼就產生了一個最核心的問題,如何根據key來獲取value的內存地址?這個過程既簡單又不簡單,由此咱們引出哈希表的知識點。編程

哈希表

哈希表是一種數據結構,實現key-value的快速存取。以前說過數組能夠實現快速存取,因此哈希表確定會使用到數組。在這裏,咱們把每個數組的單元叫作一個bucket(桶)。swift

CoreFoundation中的桶:數組

typedef struct {
    CFIndex idx;
    uintptr_t weak_key;
    uintptr_t weak_value;
    uintptr_t count;
} CFBasicHashBucket;

PHP中的桶:數據結構

typedef struct bucket {
    ulong h;                        /* Used for numeric indexing */
    uint nKeyLength;
    void *pData;
    void *pDataPtr;
    struct bucket *pListNext;
    struct bucket *pListLast;
    struct bucket *pNext;
    struct bucket *pLast;
    char arKey[1]; /* Must be last element */
} Bucket;

經過上邊的結構體,咱們知道了,在數組中存放的是一個一個的桶,那麼應該如何經過key來獲取到指定的桶呢?函數

這時候就須要一個函數來實現這樣一個功能,經過key計算出桶的內存地址。這個函數就叫作哈希函數F(x)。在現實中,考慮到性能的緣由,這個哈希函數不會被設計成完美函數,即一個key只生成一個結果。有可能兩個不一樣的key1,key2經過哈希函數獲得了同一個值,這時候咱們就稱key1,key2爲同義詞,所以就產生了衝突,這個衝突每每發生在插值的過程當中。性能

既然產生了衝突,就要想辦法解決衝突。解決衝突的方式主要分兩類 開放定址法(Open addressing)這種方法就是在計算一個key的哈希的時候,發現目標地址已經有值了,即發生衝突了,這個時候經過相應的函數在此地址後面的地址去找,直到沒有衝突爲止。這個方法經常使用的有線性探測,二次探測,再哈希。 這種解決方法有個很差的地方就是,當發生衝突以後,會在以後的地址空間中找一個放進去,這樣就有可能後來出現一個key哈希出來的結果也正好是它放進去的這個地址空間,這樣就會出現非同義詞的兩個key發生衝突。ui

連接法(Separate chaining)連接法是經過數組和鏈表組合而成的。當發生衝突的時候只要將其加到對應的鏈表中便可。

與開放定址法相比,連接法有以下幾個優勢:

  • 連接法處理衝突簡單,且無堆積現象,即非同義詞決不會發生衝突,所以平均查找長度較短;
  • 因爲連接法中各鏈表上的結點空間是動態申請的,故它更適合於造表前沒法肯定表長的狀況;
  • 開放定址法爲減小衝突,要求裝填因子α較小,故當結點規模較大時會浪費不少空間。而連接法中可取α≥1,且結點較大時,拉鍊法中增長的指針域可忽略不計,所以節省空間;
  • 在用連接法構造的散列表中,刪除結點的操做易於實現。只要簡單地刪去鏈表上相應的結點便可。而對開放地址法構造的散列表,刪除結點不能簡單地將被刪結點的空間置爲空,不然將截斷在它以後填人散列表的同義詞結點的查找路徑。這是由於各類開放地址法中,空地址單元(即開放地址)都是查找失敗的條件。所以在 用開放地址法處理衝突的散列表上執行刪除操做,只能在被刪結點上作刪除標記,而不能真正刪除結點。

固然連接法也有其缺點,拉鍊法的缺點是:指針須要額外的空間,故當結點規模較小時,開放定址法較爲節省空間,而若將節省的指針空間用來擴大散列表的規模,可以使裝填因子變小,這又減小了開放定址法中的衝突,從而提升平均查找速度。

負載因子 Load factor a=哈希表的實際元素數目(n)/ 哈希表的容量(m) a越大,哈希表衝突的機率越大,可是a越接近0,那麼哈希表的空間就越浪費。 通常狀況下建議Load factor的值爲0-0.7,Java實現的HashMap默認的Load factor的值爲0.75,當裝載因子大於這個值的時候,HashMap會對數組進行擴張至原來兩倍大。

不論使用了哪一種碰撞解決策略,都致使插入和查找操做的時間複雜度再也不是O(1)。以查找爲例,不能經過key定位到桶就結束,必須還要比較原始key(即未作哈希以前的key)是否相等,若是不相等,則要使用與插入相同的算法繼續查找,直到找到匹配的值或確認數據不在哈希表中。

PHP是使用單鏈表存儲碰撞的數據,所以實際上PHP哈希表的平均查找複雜度爲O(L),其中L爲桶鏈表的平均長度;而最壞複雜度爲O(N),此時全部數據所有碰撞,哈希表退化成單鏈表。下圖PHP中正常哈希表和退化哈希表的示意圖。

總結

哈希和哈希表的概念是這篇文章的重點,即便開發中用到的機率不高。

參考

  1. PHP哈希表碰撞攻擊原理
  2. 談談 Hash Table
  3. 深刻理解哈希表
相關文章
相關標籤/搜索