最近看PHP數組底層結構,用到了哈希表,所以還是老老實實回去看結構,在這里去總結一下。
1.哈希表的定義
這里先說一下哈希(hash)表的定義:哈希表是一種根據關鍵碼去尋找值的數據映射結構,該結構通過把關鍵碼映射的位置去尋找存放值的地方,說起來可能感覺有點復雜,我想我舉個例子你就會明白了,最典型的的例子就是字典,大家估計小學的時候也用過不少新華字典吧,如果我想要獲取“按”字詳細信息,我肯定會去根據拼音an去查找 拼音索引(當然也可以是偏旁索引),我們首先去查an在字典的位置,查了一下得到“安”,結果如下。這過程就是鍵碼映射,在公式里面,就是通過key去查找f(key)。其中,按就是關鍵字(key),f()就是字典索引,也就是哈希函數,查到的頁碼4就是哈希值。
通過字典查詢數據
2.哈希沖突
但是問題又來了,我們要查的是“按”,而不是“安,但是他們的拼音都是一樣的。也就是通過關鍵字按和關鍵字安可以映射到一樣的字典頁碼4的位置,這就是哈希沖突(也叫哈希碰撞),在公式上表達就是key1≠key2,但f(key1)=f(key2)。沖突會給查找帶來麻煩,你想想,你本來查找的是“按”,但是卻找到“安”字,你又得向后翻一兩頁,在計算機里面也是一樣道理的。
但哈希沖突是無可避免的,為什么這么說呢,因為你如果要完全避開這種情況,你只能每個字典去新開一個頁,然后每個字在索引里面都有對應的頁碼,這就可以避免沖突。但是會導致空間增大(每個字都有一頁)。
既然無法避免,就只能盡量減少沖突帶來的損失,而一個好的哈希函數需要有以下特點:
1.盡量使關鍵字對應的記錄均勻分配在哈希表里面(比如說某廠商賣30棟房子,均勻划分ABC3個區域,如果你划分A區域1個房子,B區域1個房子,C區域28個房子,有人來查找C區域的某個房子最壞的情況就是要找28次)。
2.關鍵字極小的變化可以引起哈希值極大的變化。
比較好的哈希函數是time33算法。PHP的數組就是把這個作為哈希函數。
核心的算法就是如下:
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; }
3.哈希沖突解決辦法
如果遇到沖突,哈希表一般是怎么解決的呢?具體方法有很多,百度也會有一堆,最常用的就是開發定址法和鏈地址法。
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; }
4.關於哈希表的性能
由於哈希表高效的特性,查找或者插入的情況在大多數情況下可以達到O(1),時間主要花在計算hash上,當然也有最壞的情況就是hash值全都映射到同一個地址上,這樣哈希表就會退化成鏈表,查找的時間復雜度變成O(n),但是這種情況比較少,只要不要把hash計算的公式外漏出去並且有人故意攻擊(用興趣的人可以搜一下基於哈希沖突的拒絕服務攻擊),一般也不會出現這種情況。
哈希沖突攻擊導致退化成鏈表
最后附上完整代碼下載地址。 點此下 載 源碼