一. 哈希的概念
首先,在顺序搜索以及二叉树搜索树中,元素存储的位置与元素的关键码之间没有对应的关系,因此查找一个元素时,必须要经过关键码的多次比较,搜索效率取决于搜索过程中元素的比较次数。
那么,我们理想的搜索方法是:可以不经过任何的比较,一次直接从中找到要搜索的元素。如果构造一种存储结构,通过某种函数使得元素的存储位置与元素的关键码之间有一一映射的关系,那么在查找过程中通过该哈希函数可以直接找到该元素。当向该存储结构中:
(1)插入元素时:根据某种函数利用待插元素的关键码计算出该元素的存储位置并按照此位置进行存放。
(2)搜索元素时:根据某种函数利用待搜索元素的关键码计算出该元素应该存储的位置,在该位置取出元素与搜索元素比较,若相等时则表示搜索成功,否则搜索失败。
该方式即为哈希(散列)方法,哈希方法中使用的某种函数称为哈希(散列)函数,构造出来的存储结构称为哈希(散列)表。
举例:数据集合={1,3,5,6,8} 哈希函数:Hash(key)=key%10
此时,将数据集合中的元素存储在哈希表中如下:
将key值代入到哈希函数中,从而取出该key值的存储位置,此时,搜索指定元素如8时,直接利用哈希函数计算出存储位置下标,将其对应的值与要搜索的值8比较,相等时,表示找到了,否则没找到。通过上述操作可以看出,搜索的速度非常快。
但存在这样一个问题:当向数据集合中插入元素11时,将元素11存储在哪儿?依旧利用key=11计算出Hash(11)=11%10=1,那么原本应该存储在下标为1的位置,但是此时下标为1的位置已经有存储的元素1,对于出现的这种现象我们称为哈希冲突!
哈希冲突:对于两个数据元素的关键码Ki和Kj(i!=j),即Ki!=Kj,有Hash(Ki)==Hash(Kj),即不同的关键码通过相同的哈希函数计算得到相同的哈希地址,这种现象称为哈希冲突或哈希碰撞。把具有不同关键码而计算出相同的哈希地址的数据元素称为同义词。那么发生这种哈希冲突应该如何处理呢?
处理哈希冲突常见有两种解决方式:闭散列和开散列。
二.闭散列处理哈希冲突
闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明哈希表中必然还有空位置,那么就可以把key存放在表中的“下一个”空位中。那如何寻找下一个空位置呢?
我们介绍以线性探测的方式寻找下一个空位置。下面举例介绍如何线性探测寻找下一个空位置去处理哈希冲突:
设关键码集合={1,3,5,6,8,11,15},哈希表的大小为10,哈希函数的设计用除留余数法,即哈希函数为Hash(Key)=Key%10。下面画图插入:
此时,利用的是闭散列中线性探测的方式处理了哈希冲突,大家也可以利用其他探测方式处理哈希冲突。
三.代码实现哈希表的插入查找删除
以下代码是利用闭散列中的线性探测的方式解决哈希冲突,从而实现哈希表的插入查找删除操作。
3.1 头文件hash.h
#pragma once //用于测试代码的打印函数名 #define HEADER printf("=============%s=============\n",__FUNCTION__) //由于此时的数组有效元素个数size并不能表示数组中有效元素是哪些?故定义数组元素的状态表示是否为有效元素 typedef enum{ Empty, //空状态 Valid, //有效状态 Deleted, //被删除状态 }State; //由于哈希表元素为键值对,故将其定义为结构体 typedef int KeyType; typedef int ValType; typedef struct HashElem{ KeyType key; ValType value; State state; }HashElem; //哈希表需要哈希函数,故定义一个函数指针 typedef size_t (*HashFunc)(KeyType key); //定义哈希表的结构体类型:包含存放哈希元素的数组以及其有效元素个数 #define HashMaxSize 1000 typedef struct HashTable{ HashElem data[HashMaxSize]; size_t size; HashFunc func; }HashTable;3.2 准备工作(即哈希函数的定义、哈希表的初始化以及销毁)
//0.定义哈希函数 size_t func(KeyType key) { return key%HashMaxSize; } //1.初始化 //思路:将哈希数组中有效元素个数置为0,将数组中的所有元素的状态都置为空状态并将哈希函数初始化。 void HashInit(HashTable* ht,HashFunc func) { ht->size=0; ht->func=func; size_t i=0; for(i=0;i<HashMaxSize;i++) { ht->data[i].state=Empty; } } //2.销毁 //思路:将哈希数组中有效元素个数恢复为0以及将数组元素状态恢复为空状态,并将哈希函数置为NULL即可 void HashDestroy(HashTable* ht) { ht->size=0; ht->func=NULL; size_t i=0; for(i=0;i<HashMaxSize;i++) { ht->data[i].state=Empty; } }
3.3 哈希表的插入操作
//3.插入 //思路:1.判定哈希表是否能继续插入,利用负载因子进行判断 // 2.根据key值计算offset // 3.从offset位置开始线性探测,找到状态为Empty状态的元素位置进行插入 // 4.哈希表的size++ void HashInsert(HashTable* ht,KeyType key,ValType value) { //非法输入 if(ht==NULL) return; //1.判断是否能继续插入,约定负载因子为0.8 if(ht->size>=HashMaxSize*0.8) return; //2.根据key值计算offset值 size_t offset=ht->func(key); //3.从offset开始线性探测,遇到第一个元素状态不是Valid时可以插入 while(1) { //插入成功 if(ht->data[offset].state!=Valid) { ht->data[offset].state=Valid; ht->data[offset].key=key; ht->data[offset].value=value; ht->size++; return; } //插入失败,约定key值重复时插入失败 else if(ht->data[offset].state==Valid&&ht->data[offset].key==key) { return; } //继续探测 else { offset++; if(offset>=HashMaxSize) { offset=0; } } } }
3.4 哈希表的查找操作
//4.查找 //思路:1.根据key计算offset // 2.从offset开始查找,每取到一个元素与key值进行比较 // (1)相同时,表示找到了 // (2)找着找着遇到元素为空状态时,表示该key值找不到 // (3)不同时,offset++继续探测 int HashFind(HashTable* ht,KeyType key,ValType* value) { //非法输入 if(ht==NULL||value==NULL) return 0; //若有效元素为0时,表示哈希表无元素,直接return 0 if(ht->size==0) return 0; //1.根据key计算offset size_t offset=ht->func(key); //2.从offset开始查找,每取到一个元素与key比较 while(1) { //(1)key值相同时,表示找到了 if(ht->data[offset].key==key&&ht->data[offset].state==Valid) { *value=ht->data[offset].value; return 1; } //(2)找着找着遇到元素为空状态时,查找失败 else if(ht->data[offset].state==Empty) { //查找失败 return 0; } //(3)key值不同时,offset++继续探测 else { offset++; offset=(offset>=HashMaxSize)?0:offset; } } }
3.5 哈希表的删除操作
//5.删除 //思路:1.判断是否能继续删除 // 2.根据key计算offset // 3.从offset开始依次判定当前key是否是要删除的key // (1)它们相等时,直接将状态置为Deleted并将size--,表示删除 // (2)当前元素为空状态时,则找不到key值 // (3)offset++继续线性探测 void HashRemove(HashTable* ht,KeyType key) { //非法输入 if(ht==NULL) return; //1.判定是否可以继续删除,当size为0时则不能删除 if(ht->size==0) return; //2.根据key计算offset size_t offset=ht->func(key); //3.从offset开始依次判定当前key值与要删除的key值 while(1) { //(1)当他们相等时,直接将状态置为Deleted从而表示删除 if(ht->data[offset].key==key&&ht->data[offset].state==Valid) { ht->data[offset].state=Deleted; ht->size--; } //(2)当前元素为空状态时,key找不到 else if(ht->data[offset].state==Empty) { return; } //(3)offset++继续线性探测 else { offset++; offset=offset>HashMaxSize?0:offset; } } }
4. 测试代码的正确与否
以下代码仅供参考如何测试代码的正确与否!
/*==========测试代码区===========*/ //测试HashInit void Test_HashInit() { HEADER; HashTable ht; HashInit(&ht,func); printf("expected 0,actual %d\n",ht.size); printf("expected %p,actual %p\n",func,ht.func); } //测试HashDestroy void Test_HashDestroy() { HEADER; HashTable ht; HashInit(&ht,func); HashDestroy(&ht); printf("expected 0,actual %d\n",ht.size); printf("expected NULL,actual %p\n",ht.func); } //测试HashInsert //为了查看插入正确,写一个打印函数 void HashPrint(HashTable* ht,const char* msg) { printf("%s\n",msg); //非法输入 if(ht==NULL) return; //打印哈希表元素在数组中的下标以及其键值对 size_t i=0; for(i=0;i<HashMaxSize;i++) { if(ht->data[i].state==Valid) { printf("[%d %d:%d] ",i,ht->data[i].key,ht->data[i].value); } } printf("\n"); } void Test_HashInsert() { HEADER; HashTable ht; HashInit(&ht,func); HashInsert(&ht,1,10); HashInsert(&ht,1001,101); HashInsert(&ht,2,20); HashInsert(&ht,1002,102); HashInsert(&ht,1,11); HashPrint(&ht,"插入元素"); } //测试hashFind void Test_HashFind() { HEADER; HashTable ht; HashInit(&ht,func); HashInsert(&ht,1,10); HashInsert(&ht,1001,101); HashInsert(&ht,2,20); HashInsert(&ht,1002,102); HashInsert(&ht,1,11); int ret; ValType value; ret=HashFind(&ht,1,&value); printf("expected 1,actual %d\n",ret); printf("expected 10,actual %d\n",value); ret=HashFind(&ht,1002,&value); printf("expected 1,actual %d\n",ret); printf("expected 102,actual %d\n",value); } //测试HashRemove void Test_HashRemove() { HEADER; HashTable ht; HashInit(&ht,func); HashInsert(&ht,1,10); HashInsert(&ht,1001,101); HashInsert(&ht,2,20); HashInsert(&ht,1002,102); HashInsert(&ht,1,11); HashRemove(&ht,1); int ret; ValType value; ret=HashFind(&ht,1001,&value); printf("expected 1,actual %d\n",ret); printf("expected 101,actual %d\n",value); HashPrint(&ht,"删除"); } /*==========主函数=========*/ int main() { Test_HashInit(); Test_HashDestroy(); Test_HashInsert(); Test_HashFind(); Test_HashRemove(); return 0; }