哈希表:通過key-value而直接進行訪問的數據結構,不用經過關鍵值間的比較,從而省去了大量處理時間。
哈希函數:選擇的最主要考慮因素——盡可能避免沖突的出現
構造哈希函數的原則是:
①函數本身便於計算;
②計算出來的地址分布均勻,即對任一關鍵字k,f(k) 對應不同地址的概率相等,目的是盡可能減少沖突。
1.直接定址法:
如果我們現在要對0-100歲的人口數字統計表,那么我們對年齡這個關鍵字就可以直接用年齡的數字作為地址。此時f(key) = key。

這個時候,我們可以得出這么個哈希函數:f(0) = 0,f(1) = 1,……,f(20) = 20。這個是根據我們自己設定的直接定址來的。人數我們可以不管,我們關心的是如何通過關鍵字找到地址。
如果我們現在要統計的是80后出生年份的人口數,那么我們對出生年份這個關鍵字可以用年份減去1980來作為地址。此時f (key) = key-1980。
假如今年是2000年,那么1980年出生的人就是20歲了,此時 f(2000) = 2000 - 1980,可以找得到地址20,地址20里保存了數據“人數500萬”。
也就是說,我們可以取關鍵字的某個線性函數值為散列地址,即:f(key) = a × key + b
這樣的散列函數優點就是簡單、均勻,也不會產生沖突,但問題是這需要事先知道關鍵字的分布情況,適合査找表較小且連續的情況。由於這樣的限制,在現實應用中,直接定址法雖然簡單,但卻並不常用。
2.數字分析法:
分析一組數據,比如一組員工的出生年月日,這時我們發現出生年月日的前幾位數字大體相同,這樣的話,出現沖突的幾率就會很大;
但是我們發現年月日的后幾位表示月份和具體日期的數字差別很大,如果用后面的數字來構成散列地址,則沖突的幾率會明顯降低。因此數字分析法就是找出數字的規律,盡可能利用這些數據來構造沖突幾率較低的散列地址。
3.折疊法:
將關鍵字分割成位數相同的幾部分,最后一部分位數可以不同,然后取這幾部分的疊加和(去除進位)作為散列地址。
4.除留余數法:
(常用:取關鍵字被某個不大於散列表表長m的數p除后所得的余數為散列地址。即 H(key) = key MOD p, p<=m。不僅可以對關鍵字直接取模,也可在折疊、平方取中等運算之后取模。對p的選擇很重要,一般取素數或m,若p選的不好,容易產生同義詞。
5. 平方取中法
當無法確定關鍵字中哪幾位分布較均勻時,可以先求出關鍵字的平方值,然后按需要取平方值的中間幾位作為哈希地址。這是因為:平方后中間幾位和關鍵字中每一位都相關,故不同關鍵字會以較高的概率產生不同的哈希地址。
6. 偽隨機數法:
采用一個偽隨機函數做哈希函數,即h(key)=random(key)。
解決沖突方法
● 開放定址法:
當發生地址沖突時,按照某種方法繼續探測哈希表中的其他存儲單元,直到找到空位置為止。這個過程可用下式描述: H i ( key ) = ( H ( key )+ d i ) mod m ( i = 1,2,…… , k ( k ≤ m – 1)) 其中: H ( key ) 為關鍵字 key 的直接哈希地址, m 為哈希表的長度, di 為每次再探測時的地址增量。
采用這種方法時,首先計算出元素的直接哈希地址 H ( key ) ,如果該存儲單元已被其他元素占用,則繼續查看地址為 H ( key ) + d 2 的存儲單元,如此重復直至找到某個存儲單元為空時,將關鍵字為 key 的數據元素存放到該單元。
增量 d 可以有不同的取法,並根據其取法有不同的稱呼:
( 1 ) d i = 1 , 2 , 3 , …… 線性探測再散列;
( 2 ) d i = 1^2 ,- 1^2 , 2^2 ,- 2^2 , k^2, -k^2…… 二次探測再散列;
( 3 ) d i = 偽隨機序列 偽隨機再散列;
● 鏈地址法:
鏈地址法解決沖突的做法是:如果哈希表空間為 0 ~ m - 1 ,設置一個由 m 個指針分量組成的一維數組 ST[ m ], 凡哈希地址為 i 的數據元素都插入到頭指針為 ST[ i ] 的鏈表中。這種方法有點近似於鄰接表的基本思想,且這種方法適合於沖突比較嚴重的情況。
● 公共溢出區法:
將哈希表分為基本表和溢出表兩部分,凡是和基本表發生沖突的元素,一律填入溢出表
C語言實現
定義一些宏與結構體
#define HashMaxSize 1000 //哈希表最大容量
#define LoadFactor 0.8 //負載因子,表示哈希表的負載能力
typedef int KeyType;
typedef int ValueType;
typedef size_t(*HashFunc)(KeyType key)//定義HashFunc是一個指向函數的指定,它可以指向函數類型有size_t且有一個int參數的函數;重定義哈希函數
typedef enum Stat{ //表示每個元素的狀態
Empty, //空,當前沒有值
Valid, //當前的值有效
Invalid //非空但無效,表示當前節點被刪除
}Stat;
typedef struct HashElem //哈希表的元素結構體
{
/* data */
KeyType key;
ValueType value;
Stat stat;
}HashElem;
typedef struct HashTable //哈希表
{
HashElem data[HashMaxSize];
size_t size; //當前有效的元素個數
HashFunc hashfunc;
}HashTable;
函數聲明
void HashTableInit(HashTable *ht,HashFunc hashfunc);
//初始化哈希表
int HashTableInsert(HashTable *ht,KeyType key,ValueType value);
int HashTableFind(HashTable *ht,KeyType key,ValueType *value,size_t *cur);
//哈希表的查找,找到返回1,並返回這個節點的value值,未找到返回0
void HashRemove(HashTable *ht,KeyType key);
//刪除值為key的結點
int HashEmpty(HashTable *ht);
//判斷哈希表是否為空
size_t HashSize(HashTable *ht);
//求哈希表的大小
void HashTableDestroy(HashTable *ht);
//銷毀哈希表
函數實現
size_t HashFuncDefault(KeyType key){
return key%HashMaxSize;//檢查是否超出最大儲存量
}
void HashTableInit(HashTable *ht){
if (ht == NULL) //非法輸入
return;
ht->size = 0;
ht->hashfunc = HashFuncDefault;//錯誤賦值
for (size_t i =0;i<HashMaxSize;i++){
ht->data[i].key = 0;
ht->dota[i].stat = Empty;
ht->data[i].value = 0;
}
}
int HashTableInsert(HashTable *ht,KeyType key,ValueType value){
if (ht == NULL)
return 0;
if (ht->size >= HashMaxSize*LoadFactor) //當哈希表的size超出了負載
return 0;
size_t cur = ht->hashfunc(key); //根據哈希函數將key轉換,求得key在哈希表中的下標
while(1){//判斷當前下標是否被占用
if (ht->data[cur].key ==key) //保證不會用重復的數字存入哈希表
return 0;
if(ht->data[cur].stat != Valid){
ht->data[cur].key =key;
ht->data[cur].value = value;
ht->data[cur].stat = Valid;
ht->size++;
return 1;
}
cur++;
}
}
int HashTableFind(HashTable *ht,KeyType key,ValueType *value){//哈希表的查找,找到返回1,沒找到返回0
if(ht == NULL)
return 0;
size_t offset=ht->hashfunc(key);//通過哈希函數找到key的下標
if(ht->data[offset].key == key && ht->data[offset].stat == Valid){//若當前下標所對應的值正好是key並且當前的狀態必須為valid才返回
*value = ht->data[offset].value;
return 1;
} else{//值不為key,根據沖突解決方法查找(當前解決方法為逐個往后查),直到找到stat等於empty
while (ht->data[offset].stat !=Empty){
if(ht->data[offset].key !=key){
offset++;
if(offset >= HashMaxSize//判斷下標是否超出最大值)
offset = 0;
} else{
if(ht->data[offset].stat == Valid){
*value =ht->data[offset].value;
return 1;
} else
offset++;
}
}
return 0;
}
}
int HashTableFindCur(HashTable *ht,KeyType key,size_t *cur){//刪除節點
if(ht == NULL)
return 0;
for(size_t i = 0;i < HashMaxSize;i++){
if(ht->data[i].key == key && ht->data[i].stat == Valid){
*cur = i;//?
return 1;
}
}
return 0;
}
void HashRemove(HashTable *ht,KeyType key){
if (ht == NULL) //非法輸入
return;
ValueType value = 0;
size_t cur =0;
int ret = HashTableFindCur(ht,key,&cur);//通過find函數得到key是否存在哈希表中
if(ret == 0)
return;
else{
ht->data[cur].stat = Invalid;
ht->size--;
}
}
int HashEmpty(HashTable *ht){
if(ht == NULL)
return 0;
else
return ht->size >0?1:0
}
size_t HashSize(HashTable *ht){//求哈希表大小
if(ht == NULL) {
ht->data->stat = Empty;
return ht->size;
} else
{
return ht->size;
}
}
void HashPrint(HashTable *ht, const char *msg){
if(ht ==NULL || ht->size = 0)
return;
printf("%s/n",msg);
for(size_t i =0;i< HashMaxSize;i++){
if(ht->data[i],stat != Empty)
printf("[%d] key=%d value=%d stat =%d\n",i,ht->data[i].key,ht->data[i].value,ht->data[i].stat);
}
}
— END —
看到這里你是不是又學到了很多新知識呢~
如果你很想學編程,小編推薦我的C語言/C++編程學習基地【點擊進入】!
都是學編程小伙伴們,帶你入個門還是簡簡單單啦,一起學習,一起加油~
還有許多學習資料和視頻,相信你會喜歡的!
涉及:游戲開發、常用軟件開發、編程基礎知識、課程設計、黑客等等......
