最近看PHP數組底層結構,用到了哈希表,因此仍是老老實實回去看結構,在這裏去總結一下。
這裏先說一下哈希(hash)表的定義:哈希表是一種根據關鍵碼去尋找值的數據映射結構,該結構經過把關鍵碼映射的位置去尋找存放值的地方,提及來可能感受有點複雜,我想我舉個例子你就會明白了,最典型的的例子就是字典,你們估計小學的時候也用過很多新華字典吧,若是我想要獲取「按」字詳細信息,我確定會去根據拼音an去查找 拼音索引(固然也能夠是偏旁索引),咱們首先去查an在字典的位置,查了一下獲得「安」,結果以下。這過程就是鍵碼映射,在公式裏面,就是經過key去查找f(key)。其中,按就是關鍵字(key),f()就是字典索引,也就是哈希函數,查到的頁碼4就是哈希值。
node
經過字典查詢數據算法
可是問題又來了,咱們要查的是「按」,而不是「安,可是他們的拼音都是同樣的。也就是經過關鍵字按和關鍵字安能夠映射到同樣的字典頁碼4的位置,這就是哈希衝突(也叫哈希碰撞),在公式上表達就是key1≠key2,但f(key1)=f(key2)。衝突會給查找帶來麻煩,你想一想,你原本查找的是「按」,可是卻找到「安」字,你又得向後翻一兩頁,在計算機裏面也是同樣道理的。數組
但哈希衝突是無可避免的,爲何這麼說呢,由於你若是要徹底避開這種狀況,你只能每一個字典去新開一個頁,而後每一個字在索引裏面都有對應的頁碼,這就能夠避免衝突。可是會致使空間增大(每一個字都有一頁)。函數
既然沒法避免,就只能儘可能減小衝突帶來的損失,而一個好的哈希函數須要有如下特色:性能
1.儘可能使關鍵字對應的記錄均勻分配在哈希表裏面(好比說某廠商賣30棟房子,均勻劃分ABC3個區域,若是你劃分A區域1個房子,B區域1個房子,C區域28個房子,有人來查找C區域的某個房子最壞的狀況就是要找28次)。ui
2.關鍵字極小的變化能夠引發哈希值極大的變化。spa
比較好的哈希函數是time33算法。PHP的數組就是把這個做爲哈希函數。指針
核心的算法就是以下:code
unsigned long hash(const char* key){ unsigned long hash=0; for(int i=0;i<strlen(key);i++){ hash = hash*33+str[i]; } return hash; }
若是遇到衝突,哈希表通常是怎麼解決的呢?具體方法有不少,百度也會有一堆,最經常使用的就是開發定址法和鏈地址法。blog
1.開發定址法
若是遇到衝突的時候怎麼辦呢?就找hash表剩下空餘的空間,找到空餘的空間而後插入。就像你去商店買東西,發現東西賣光了,怎麼辦呢?找下一家有東西賣的商家買唄。
因爲我沒有深刻試驗過,因此貼上在書上的解釋:
2.鏈地址法
上面所說的開發定址法的原理是遇到衝突的時候查找順着原來哈希地址查找下一個空閒地址而後插入,可是也有一個問題就是若是空間不足,那他沒法處理衝突也沒法插入數據,所以須要裝填因子(插入數據/空間)<=1。
那有沒有一種方法能夠解決這種問題呢?鏈地址法能夠,鏈地址法的原理時若是遇到衝突,他就會在原地址新建一個空間,而後以鏈表結點的形式插入到該空間。我感受業界上用的最多的就是鏈地址法。下面從百度上截取來一張圖片,能夠很清晰明瞭反應下面的結構。好比說我有一堆數據{1,12,26,337,353...},而個人哈希算法是H(key)=key mod 16,第一個數據1的哈希值f(1)=1,插入到1結點的後面,第二個數據12的哈希值f(12)=12,插入到12結點,第三個數據26的哈希值f(26)=10,插入到10結點後面,第4個數據337,計算獲得哈希值是1,遇到衝突,可是依然只須要找到該1結點的最後鏈結點插入便可,同理353。
哈希表的拉鍊法實現
下面解析一下如何用C++實現鏈地址法。
第一步。
確定是構建哈希表。
首先定義鏈結點,以結構體Node展現,其中Node有三個屬性,一個是key值,一個value值,還有一個是做爲鏈表的指針。還有做爲類的哈希表。
#define HASHSIZE 10 typedef unsigned int uint; typedef struct Node{ const char* key; const char* value; Node *next; }Node; class HashTable{ private: Node* node[HASHSIZE]; public: HashTable(); uint hash(const char* key); Node* lookup(const char* key); bool install(const char* key,const char* value); const char* get(const char* key); void display(); };
而後定義哈希表的構造方法
HashTable::HashTable(){ for (int i = 0; i < HASHSIZE; ++i) { node[i] = NULL; } }
第二步。
定義哈希表的Hash算法,在這裏我使用time33算法。
uint HashTable::hash(const char* key){ uint hash=0; for (; *key; ++key) { hash=hash*33+*key; } return hash%HASHSIZE; }
第三步。
定義一個查找根據key查找結點的方法,首先是用Hash函數計算頭地址,而後根據頭地址向下一個個去查找結點,若是結點的key和查找的key值相同,則匹配成功。
Node* HashTable::lookup(const char* key){ Node *np; uint index; index = hash(key); for(np=node[index];np;np=np->next){ if(!strcmp(key,np->key)) return np; } return NULL; }
第四步。
定義一個插入結點的方法,首先是查看該key值的結點是否存在,若是存在則更改value值就好,若是不存在,則插入新結點。
bool HashTable::install(const char* key,const char* value){ uint index; Node *np; if(!(np=lookup(key))){ index = hash(key); np = (Node*)malloc(sizeof(Node)); if(!np) return false; np->key=key; np->next = node[index]; node[index] = np; } np->value=value; return true; }
因爲哈希表高效的特性,查找或者插入的狀況在大多數狀況下能夠達到O(1),時間主要花在計算hash上,固然也有最壞的狀況就是hash值全都映射到同一個地址上,這樣哈希表就會退化成鏈表,查找的時間複雜度變成O(n),可是這種狀況比較少,只要不要把hash計算的公式外漏出去而且有人故意攻擊(用興趣的人能夠搜一下基於哈希衝突的拒絕服務攻擊),通常也不會出現這種狀況。
哈希衝突攻擊致使退化成鏈表
最後附上完整代碼下載地址。 點此下 載 源碼