拋磚引玉,你們有沒有想過Object-C中,NSDictionary是如何實現根據key快速查值的?php
在編程的世界中,比較重要的數據結構有如下3個:html
咱們都知道,數組是存儲數據的自然載體,在內存中是一段連續的地址,正是因爲這個特性,咱們可以經過數組的下標快速的獲取與之對應的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)連接法是經過數組和鏈表組合而成的。當發生衝突的時候只要將其加到對應的鏈表中便可。
與開放定址法相比,連接法有以下幾個優勢:
固然連接法也有其缺點,拉鍊法的缺點是:指針須要額外的空間,故當結點規模較小時,開放定址法較爲節省空間,而若將節省的指針空間用來擴大散列表的規模,可以使裝填因子變小,這又減小了開放定址法中的衝突,從而提升平均查找速度。
負載因子 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中正常哈希表和退化哈希表的示意圖。
哈希和哈希表的概念是這篇文章的重點,即便開發中用到的機率不高。