哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。數組
順序搜索以及二叉樹搜索樹中,元素存儲位置和元素各關鍵碼之間沒有對應的關係,所以在查找一個元素時,必需要通過關鍵碼的屢次比較。搜索的效率取決於搜索過程當中元素的比較次數。數據結構
理想的搜索方法:能夠不通過任何比較,一次直接從表中獲得要搜索的元素。
若是構造一種存儲結構,經過某種函數(hashFunc)使元素的存儲位置與它的關鍵碼之間可以創建一一映射的關係,那麼在查找時經過該函數能夠很快找到該元素。dom
當向該結構中:
插入元素時:根據待插入元素的關鍵碼,以此函數計算出該元素的存儲位置並按此位置進行存放
搜索元素時:對元素的關鍵碼進行一樣的計算,把求得的函數值當作元素的存儲位置,在結構中按此位置取元素比較,若關鍵碼相等,則搜索成功
該方式即爲哈希(散列)方法,哈希方法中使用的轉換函數稱爲哈希(散列)函數,構造出來的結構稱爲哈希表(Hash Table)(或者
稱散列表)
例如:數據集合{180,750,600,430,541,900,460}
用該方法進行搜索沒必要進行屢次關鍵碼的比較,所以搜索的速度比較快
問題:按照上述哈希方式,向集合中插入元素443,會出現什麼問題?
這回就要引出一個概念叫哈希衝突:對於兩個數據元素的關鍵字 和 (i !=j),有 != ,但有:HashFun(Ki) == HashFun(Kj)即不一樣關鍵字經過相同哈希哈數計算出相同的哈希地址,該種現象稱爲哈希衝突或哈希碰撞。把具備不一樣關鍵碼而具備相同哈希地址的數據元素稱爲「同義詞」。函數
解決哈希衝突兩種常見的方法是:閉散列和開散列
閉散列:
閉散列:也叫開放地址法,當發生哈希衝突時,若是哈希表未被裝滿,說明在哈希表中必然還有空位置,那麼能夠把key存放到表中「下一個」 空位中去
那如何尋找下一個空餘位置? 這裏就要用到兩種方法:線性探測和二次探測
線性探測
設關鍵碼集合爲{37, 25, 14, 36, 49, 68, 57, 11},散列表爲HT[12],表的大小m = 12,假設哈希函數爲:Hash(x) = x %p(p = 11,是最接近m的質數),就有:
Hash(37) = 4
Hash(25) = 3
Hash(14) = 3
Hash(36) = 3
Hash(49) = 5
Hash(68) = 2
Hash(57) = 2
Hash(11) = 0
其中25,14,36以及68,57發生哈希衝突,一旦衝突必需要找出下一個空餘位置
線性探測找的處理爲:從發生衝突的位置開始,依次繼續向後探測,直到找到空位置爲止
【插入】
1). 使用哈希函數找到待插入元素在哈希表中的位置
2). 若是該位置中沒有元素則直接插入新元素;若是該位置中有元素且和待插入元素相同,則不用插入;若是該位置中有元素但不是待插入元素則發生哈希衝突,使用線性探測找到下一個空位置,插入新元素;
採用線性探測,實現起來很是簡單,缺陷是:
一旦發生哈希衝突,全部的衝突連在一塊兒,容易產生數據「堆積」,即:不一樣關鍵碼佔據了可利用的空位置,使得尋找某關鍵碼的位置須要許屢次比較,致使搜索效率下降。 如何緩解呢? 引入新概念負載因子(負載因子的應用在下一篇博文)和二次探測
二次探測
發生哈希衝突時,二次探查法在表中尋找「下一個」空位置的公式爲:
Hi= (Ho + i^2) % m,Hi = (Ho -i^2 ) % m, i = 1,2,3…,(m-1)/Ho. 是經過散列函數Hash(x)對元素的關鍵碼 key 進行計算獲得的位置,m是表的大小假設數組的關鍵碼爲37, 25, 14, 36, 49, 68, 57, 11,取m = 19,這樣可設定爲HT[19],採用散列函數Hash(x) = x % 19,則:
Hash(37)=18
Hash(25)=6
Hash(14)=14
Hash(36)=17
Hash(49)=11
Hash(68)=11
Hash(57)=0
Hash(11)=11
採用二次探測處理哈希衝突:
研究代表:當表的長度爲質數且表裝載因子a不超過0.5時,新的表項必定可以插入,並且任何一個位置都不會被探查兩次。所以只要表中有一半的空位置,就不會存在表滿的問題。在搜索時能夠不考慮表裝滿的狀況,但在插入時必須確保表的裝載因子a不超過0.5;若是超出必須考慮增容測試
開散列法又叫鏈地址法(開鏈法)。(將在下一篇博文中寫出)
開散列法:首先對關鍵碼集合用散列函數計算散列地址,具備相同地址的關鍵碼歸於同一子集合,每個子集合稱爲一個桶,各個桶中的元素經過一個單鏈表連接起來,各鏈表的頭結點存儲在哈希表中。.net
設元素的關鍵碼爲37, 25, 14, 36, 49, 68, 57, 11, 散列表爲HT[12],表的大小爲12,散列函數爲Hash(x) = x % 11
Hash(37)=4
Hash(25)=3
Hash(14)=3
Hash(36)=3
Hash(49)=5
Hash(68)=2
Hash(57)=2
Hash(11)=0
使用哈希函數計算出每一個元素所在的桶號,同一個桶的鏈表中存放哈希衝突的元素。
一般,每一個桶對應的鏈表結點都不多,將n個關鍵碼經過某一個散列函數,存放到散列表中的m個桶中,那麼每個桶中鏈表的平均長度爲。以搜索平均長度爲的鏈表代替了搜索長度爲 n 的順序表,搜索效率快的多。
應用鏈地址法處理溢出,須要增設連接指針,彷佛增長了存儲開銷。事實上:
因爲開地址法必須保持大量的空閒空間以確保搜索效率,如二次探查法要求裝載因子a <= 0.7,而表項所佔空間又比指針大的多,因此使用鏈地址法反而比開地址法節省存儲空間。翻譯
引發哈希衝突的一個緣由多是:哈希函數設計不夠合理。
哈希函數設計原則:
.哈希函數的定義域必須包括須要存儲的所有關鍵碼,而若是散列表容許有m個地址時,其值域必須在0到m-1之間
.哈希函數計算出來的地址能均勻分佈在整個空間中
.哈希函數應該比較簡單
下面簡單介紹了一些哈希函數:
1.直接定址法
取關鍵字的某個線性函數爲散列地址:Hash(Key)= A*Key + B
優勢:簡單、均勻
缺點:須要事先知道關鍵字的分佈狀況
適合查找比較小且連續的狀況
2.除留餘數法
設散列表中容許的地址數爲m,取一個不大於m,但最接近或者等於m的質數p做爲除數,按照哈希函數:Hash(key) = key% p(p<=m),將關鍵碼轉換成哈希地址3.平方取中法
假設關鍵字爲1234,對它平方就是1522756,抽取中間的3位227做爲哈希地址;
再好比關鍵字爲4321,對它平方就是18671041,抽取中間的3位671(或710)做爲哈希地址
平方取中法比較適合:不知道關鍵字的分佈,而位數又不是很大的狀況
4.摺疊法
摺疊法是將關鍵字從左到右分割成位數相等的幾部分(最後一部分位數能夠短些),而後將這幾部分疊加求和,並按散列表表長,取後幾位做爲散列地址摺疊法適合事先不須要知道關鍵字的分佈,適合關鍵字位數比較多的狀況
5.隨機數法
選擇一個隨機函數,取關鍵字的隨機函數值爲它的哈希地址,即H(key) = random(key),其中random爲隨機數函數一般應用於關鍵字長度不等時採用此法
6.數學分析法
設有n個d位數,每一位可能有r種不一樣的符號,這r種不一樣的符號在各位上出現的頻率不必定相同,可能在某些位上分佈比較均勻,每種符號出現的機會均等,在某些位上分佈不均勻只有某幾種符號常常出現。可根據散列表的大小,選擇其中各類符號分佈均勻的若干位做爲散列地址。
例如:假設要存儲某家公司員工登記表,若是用手機號做爲關鍵字,那麼極有可能前7位都是 相同的,那麼咱們能夠選擇後面的四位做爲散列地址,若是這樣的抽取工做還容易出現 衝突,還能夠對抽取出來的數字進行反轉(如1234改爲4321)、右環位移(如1234改爲4123)、左環移位、前兩數與後兩數疊加(如1234改爲12+34=46)等方法
說了這麼多概念,來看看代碼。
哈希表的結構定義:設計
typedef int KeyType; typedef int ValueType; typedef enum Status { EMPTY, EXIST, DELETE, }Status; typedef struct HashNode { KeyType _key; ValueType _value; Status _status; }HashNode; typedef struct HashTable { HashNode *_table; size_t _size; size_t _N; }HashTable;
哈希表的初始化:指針
void HashTableInit(HashTable* ht) //初始化 { size_t i = 0; assert(ht); ht->_size = 0; ht->_N = HashTablePrime(0); ht->_table = (HashNode *)malloc(sizeof(HashNode)*ht->_N); assert(ht->_table); for (i=0; i<ht->_N; i++) ht->_table[i]._status = EMPTY; }
哈希函數:調試
KeyType HashFunc(KeyType key,size_t n) { return key%n; }
看看哈希表的插入:(這裏處理哈希衝突時採用線性探測,二次探測將在下一次博客中寫出)
擴容時要特別注意,不能簡單的用malloc和realloc開出空間後直接付給哈希表,必定記得擴容以後須要從新映射原表的全部值。
int HashTableInsert(HashTable* ht, KeyType key, ValueType value) //插入 { KeyType index = key; assert(ht); **if (ht->_N == ht->_size) //擴容 { KeyType index; size_t newN = HashTablePrime(ht->_N); HashNode *tmp = (HashNode *)malloc(sizeof(HashNode)*newN); size_t i = 0; assert(tmp); //HashTablePrint(ht); //擴容調試使用 for (i=0; i<newN; i++) tmp[i]._status = EMPTY; for (i=0; i<ht->_N; i++) //擴容以後把之前的表中元素從新映射 { if (ht->_table[i]._status == EXIST) //原表存在時 { index = HashFunc(ht->_table[i]._key,newN); if (tmp[index]._status == EXIST) //發生哈希衝突時 { while (1) { index +=1; if ((size_t)index > newN) index %= newN; if (tmp[index]._status != EXIST) break; } } tmp[index]._key = ht->_table[i]._key; tmp[index]._value = ht->_table[i]._value; tmp[index]._status = EXIST; } else tmp[i]._status = ht->_table[i]._status; } ht->_table = tmp; ht->_N = newN; }** index = HashFunc(key,ht->_N); if (ht->_table[index]._status == EXIST) //發生哈希衝突 { size_t i = 0; for (i=0; i<ht->_N;i++ ) { if (ht->_table[index]._key == key) return -1; index +=i; if ((size_t)index >ht->_N) index %= ht->_N; if (ht->_table[index]._status != EXIST) break; } } ht->_table[index]._key = key; ht->_table[index]._value = value; ht->_table[index]._status = EXIST; ht->_size++; return 0; }
哈希表的查找:
HashNode* HashTableFind(HashTable* ht, KeyType key) //查找 { size_t i = 0; KeyType index = key; assert(ht); index = HashFunc(key,ht->_N); if (ht->_table[index]._key == key) return &(ht->_table[index]); else { for (i=0; i<ht->_N; i++) { index += i; if (ht->_table[index]._key == key) return &(ht->_table[index]); if (ht->_table[index]._status == EMPTY) return NULL; } } return NULL; }
哈希表的刪除:
int HashTableRemove(HashTable* ht, KeyType key) //刪除 { assert(ht); if(HashTableFind(ht,key)) { HashTableFind(ht,key)->_status = DELETE; return 0; } else return -1; }
哈希表的銷燬:(使用了malloc開闢空間必須手動銷燬)
void HashTableDestory(HashTable* ht)//銷燬 { free(ht->_table); ht->_table = NULL; ht->_size = 0; ht->_N = 0; }
哈希表的打印:
void HashTablePrint(HashTable *ht) //打印hash表 { size_t i = 0; assert(ht); for (i=0; i<ht->_N; i++) { if (ht->_table[i]._status == EXIST) printf("[%d]%d ",i,ht->_table[i]._key); else if (ht->_table[i]._status == EMPTY) printf("[%d]E ",i); else printf("[%d]D ",i); } printf("\n\n"); }
哈希表整個在插入這塊會比較ran,要仔細理解,特別是擴容那塊。
測試案例:
void TestHashTable() { HashTable ht; HashTableInit(&ht); HashTableInsert(&ht,53,0); HashTableInsert(&ht,54,0); HashTableInsert(&ht,55,0); HashTableInsert(&ht,106,0); HashTableInsert(&ht,1,0); HashTableInsert(&ht,15,0); HashTableInsert(&ht,10,0); HashTablePrint(&ht); printf("%d ",HashTableFind(&ht,53)->_key); printf("%d ",HashTableFind(&ht,54)->_key); printf("%d ",HashTableFind(&ht,10)->_key); printf("%d ",HashTableFind(&ht,15)->_key); printf("%p ",HashTableFind(&ht,3)); printf("\n\n"); HashTableRemove(&ht,53); HashTableRemove(&ht,54); HashTableRemove(&ht,106); HashTableRemove(&ht,10); HashTableRemove(&ht,5); HashTablePrint(&ht); HashTableInsert(&ht,53,0); HashTableInsert(&ht,54,0); HashTableInsert(&ht,106,0); HashTablePrint(&ht); HashTableDestory(&ht); HashTablePrint(&ht); }
測試結果:
更多內容請關注本文博客:請戳關注連接
如需轉載和翻譯請聯繫本人。