Hash表也稱散列表,也有直接譯做哈希表,Hash表是一種特殊的數據結構,它同數組、鏈表以及二叉排序樹等相比較有很明顯的區別,它可以快速定位到想要查找的記錄,而不是與表中存在的記錄的關鍵字進行比較來進行查找。這個源於Hash表設計的特殊性,它採用了函數映射的思想將記錄的存儲位置與記錄的關鍵字關聯起來,從而可以很快速地進行查找。數組
1.Hash表的設計思想數據結構
對於通常的線性表,好比鏈表,若是要存儲聯繫人信息: 函數
張三 13980593357 李四 15828662334 王五 13409821234 張帥 13890583472
那麼可能會設計一個結構體包含姓名,手機號碼這些信息,而後把4個聯繫人的信息存到一張鏈表中。當要查找」李四 15828662334「這條記錄是否在這張鏈表中或者想要獲得李四的手機號碼時,可能會從鏈表的頭結點開始遍歷,依次將每一個結點中的姓名同」李四「進行比較,直到查找成功或者失敗爲止,這種作法的時間複雜度爲O(n)。即便採用二叉排序樹進行存儲,也最多爲O(logn)。假設可以經過」李四「這個信息直接獲取到該記錄在表中的存儲位置,就能省掉中間關鍵字比較的這個環節,複雜度直接降到O(1)。Hash表就可以達到這樣的效果。性能
Hash表採用一個映射函數 f : key —> address 將關鍵字映射到該記錄在表中的存儲位置,從而在想要查找該記錄時,能夠直接根據關鍵字和映射關係計算出該記錄在表中的存儲位置,一般狀況下,這種映射關係稱做爲Hash函數,而經過Hash函數和關鍵字計算出來的存儲位置(注意這裏的存儲位置只是表中的存儲位置,並非實際的物理地址)稱做爲Hash地址。好比上述例子中,假如聯繫人信息採用Hash表存儲,則當想要找到「李四」的信息時,直接根據「李四」和Hash函數計算出Hash地址便可。下面討論一下Hash表設計中的幾個關鍵問題。spa
1. Hash函數的設計設計
Hash函數設計的好壞直接影響到對Hash表的操做效率。下面舉例說明:code
假如對上述的聯繫人信息進行存儲時,採用的Hash函數爲:姓名的每一個字的拼音開頭大寫字母的ASCII碼之和。blog
所以address(張三)=ASCII(Z)+ASCII(S)=90+83=173;排序
address(李四)=ASCII(L)+ASCII(S)=76+83=159;get
address(王五)=ASCII(W)+ASCII(W)=87+87=174;
address(張帥)=ASCII(Z)+ASCII(S)=90+83=173;
假如只有這4個聯繫人信息須要進行存儲,這個Hash函數設計的很糟糕。首先,它浪費了大量的存儲空間,假如採用char型數組存儲聯繫人信息的話,則至少須要開闢174*12字節的空間,空間利用率只有4/174,不到5%;另外,根據Hash函數計算結果以後,address(張三)和address(李四)具備相同的地址,這種現象稱做衝突,對於174個存儲空間中只須要存儲4條記錄就發生了衝突,這樣的Hash函數設計是很不合理的。因此在構造Hash函數時應儘可能考慮關鍵字的分佈特色來設計函數使得Hash地址隨機均勻地分佈在整個地址空間當中。一般有如下幾種構造Hash函數的方法:
1)直接定址法
取關鍵字或者關鍵字的某個線性函數爲Hash地址,即address(key)=a*key+b;如知道學生的學號從2000開始,最大爲4000,則能夠將address(key)=key-2000做爲Hash地址。
2)平方取中法
對關鍵字進行平方運算,而後取結果的中間幾位做爲Hash地址。假若有如下關鍵字序列{421,423,436},平方以後的結果爲{177241,178929,190096},那麼能夠取{72,89,00}做爲Hash地址。
3)摺疊法
將關鍵字拆分紅幾部分,而後將這幾部分組合在一塊兒,以特定的方式進行轉化造成Hash地址。假如知道圖書的ISBN號爲8903-241-23,能夠將address(key)=89+03+24+12+3做爲Hash地址。
4)除留取餘法
若是知道Hash表的最大長度爲m,能夠取不大於m的最大質數p,而後對關鍵字進行取餘運算,address(key)=key%p。
在這裏p的選取很是關鍵,p選擇的好的話,可以最大程度地減小衝突,p通常取不大於m的最大質數。
2.Hash表大小的肯定
Hash表大小的肯定也很是關鍵,若是Hash表的空間遠遠大於最後實際存儲的記錄個數,則形成了很大的空間浪費,若是選取小了的話,則容易形成衝突。在實際狀況中,通常須要根據最終記錄存儲個數和關鍵字的分佈特色來肯定Hash表的大小。還有一種狀況時可能事先不知道最終須要存儲的記錄個數,則須要動態維護Hash表的容量,此時可能須要從新計算Hash地址。
3.衝突的解決
在上述例子中,發生了衝突現象,所以須要辦法來解決,不然記錄沒法進行正確的存儲。一般狀況下有2種解決辦法:
1)開放定址法
即當一個關鍵字和另外一個關鍵字發生衝突時,使用某種探測技術在Hash表中造成一個探測序列,而後沿着這個探測序列依次查找下去,當碰到一個空的單元時,則插入其中。比較經常使用的探測方法有線性探測法,好比有一組關鍵字{12,13,25,23,38,34,6,84,91},Hash表長爲14,Hash函數爲address(key)=key%11,當插入12,13,25時能夠直接插入,而當插入23時,地址1被佔用了,所以沿着地址1依次往下探測(探測步長能夠根據狀況而定),直到探測到地址4,發現爲空,則將23插入其中。
2)鏈地址法
採用數組和鏈表相結合的辦法,將Hash地址相同的記錄存儲在一張線性表中,而每張表的表頭的序號即爲計算獲得的Hash地址。如上述例子中,採用鏈地址法造成的Hash表存儲表示爲:
雖然可以採用一些辦法去減小衝突,可是衝突是沒法徹底避免的。所以須要根據實際狀況選取解決衝突的辦法。
4.Hash表的平均查找長度
Hash表的平均查找長度包括查找成功時的平均查找長度和查找失敗時的平均查找長度。
查找成功時的平均查找長度=表中每一個元素查找成功時的比較次數之和/表中元素個數;
查找不成功時的平均查找長度至關於在表中查找元素不成功時的平均比較次數,能夠理解爲向表中插入某個元素,該元素在每一個位置都有可能,而後計算出在每一個位置可以插入時須要比較的次數,再除以表長即爲查找不成功時的平均查找長度。
下面舉個例子:
有一組關鍵字{23,12,14,2,3,5},表長爲14,Hash函數爲key%11,則關鍵字在表中的存儲以下:
地址 0 1 2 3 4 5 6 7 8 9 10 11 12 13
關鍵字 23 12 14 2 3 5
比較次數 1 2 1 3 3 2
所以查找成功時的平均查找長度爲(1+2+1+3+3+2)/6=11/6;
查找失敗時的平均查找長度爲(1+7+6+5+4+3+2+1+1+1+1+1+1+1)/14=38/14;
這裏有一個概念裝填因子=表中的記錄數/哈希表的長度,若是裝填因子越小,代表表中還有不少的空單元,則發生衝突的可能性越小;而裝填因子越大,則發生衝突的可能性就越大,在查找時所耗費的時間就越多。所以,Hash表的平均查找長度和裝填因子有關。有相關文獻證實當裝填因子在0.5左右的時候,Hash的性能可以達到最優。所以,通常狀況下,裝填因子取經驗值0.5。
5.Hash表的優缺點
Hash表存在的優勢顯而易見,可以在常數級的時間複雜度上進行查找,而且插入數據和刪除數據比較容易。可是它也有某些缺點,好比不支持排序,通常比用線性表存儲須要更多的空間,而且記錄的關鍵字不能重複。
代碼實現:
/*Hash表,採用數組實現,2012.9.28*/ #include<stdio.h> #define DataType int #define M 30 typedef struct HashNode { DataType data; //存儲值 int isNull; //標誌該位置是否已被填充 }HashTable; HashTable hashTable[M]; void initHashTable() //對hash表進行初始化 { int i; for(i = 0; i<M; i++) { hashTable[i].isNull = 1; //初始狀態爲空 } } int getHashAddress(DataType key) //Hash函數 { return key % 29; //Hash函數爲 key%29 } int insert(DataType key) //向hash表中插入元素 { int address = getHashAddress(key); if(hashTable[address].isNull == 1) //沒有發生衝突 { hashTable[address].data = key; hashTable[address].isNull = 0; } else //當發生衝突的時候 { while(hashTable[address].isNull == 0 && address<M) { address++; //採用線性探測法,步長爲1 } if(address == M) //Hash表發生溢出 return -1; hashTable[address].data = key; hashTable[address].isNull = 0; } return 0; } int find(DataType key) //進行查找 { int address = getHashAddress(key); while( !(hashTable[address].isNull == 0 && hashTable[address].data == key && address<M)) { address++; } if( address == M) address = -1; return address; } int main(int argc, char *argv[]) { int key[]={123,456,7000,8,1,13,11,555,425,393,212,546,2,99,196}; int i; initHashTable(); for(i = 0; i<15; i++) { insert(key[i]); } for(i = 0; i<15; i++) { int address; address = find(key[i]); printf("%d %d\n", key[i],address); } return 0; }