哈希概念
在之前学习过的顺序搜索和二叉树搜索中,元素存储位置和元素各关键码之间没有对应关系,因此在查找一个元素时,必须要经过关键码的多次比较。搜索的效率取决于搜索过程中元素的比较次数。
我们希望可以不经过任何比较,一次直接从表中得到想要的元素,这样一来,搜索效率就有了质的提高。如果构造一种存储结构,通过某种函数是元素的存储位置与他的关键码之间能够建立一一映射的关系,那么在查找的时候通过该函数就可以很快的找到该元素。
当向该结构中:
插入元素时:根据待插入元素关键码,以此计算出该元素的存储位置进行存放
搜索元素时:对元素的关键码进行同样的计算,把求的函数值当做元素的位置,在结构中按此位置取元素比较,若相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(散列表)。
哈希冲突
我们使用同一个哈希函数来计算不止一个的待存放的数据在表中的存放位置,总是会有一些数据通过这个转换函数计算出来的存放位置是相同的,这就是哈希冲突。也就是说,不同的关键字通过同一哈希转换函数计算出相同的哈希地址。
处理哈希冲突
针对哈希冲突,这里有两种解决办法:闭散列和开散列
闭散列:(开放地址法或者也叫线性探测法)
当我们要往哈希表中插入一个数据时,通过哈希函数计算该值的哈希地址,当我们找到哈希地址时却发现该位置已经被别的数据插入了,那么此时我们就找紧跟着这一位置的下一个位置,看是否能够插入,如果能则插入,不能则继续探测紧跟着当前位置的下一个位置。
下面我们使用C语言来实现哈希表的基本操作:
线性探测法实现:
//hash.h文件内容
#pragma once
#define max_size 1000
typedef int KeyType;
typedef int ValType;
typedef char DataType;
typedef int (*HashFunc)(KeyType key);
typedef enum Stat
{
Deleted,//删除状态
Valid,//有效状态
Empty,//空状态(即无效状态)
}Stat;
typedef struct HashElem
{
KeyType key;
ValType value;
Stat stat;
}HashElem;
typedef struct HashTable
{
HashElem data[max_size];
int size;//有效元素个数
HashFunc func;//哈希函数
}HashTable;
//初始化
void HashInit(HashTable *ht,HashFunc Hash_func);
//销毁
void HashDestroy(HashTable *ht);
//插入数据
void HashInsert(HashTable *ht,KeyType key,ValType value);
//查找数据
int HashFind(HashTable *ht,KeyType key,ValType *value);
//删除数据
void HashRemove(HashTable *ht,KeyType key);
以下是对应函数的实现以测试结果:
//初始化
void HashInit(HashTable *ht,HashFunc Hash_func)
{
if(ht == NULL)
{
//非法输入
return;
}
ht->size = 0;
ht->func = Hash_func;
int i = 0;
for(;i < max_size;i++)
{
//将哈希表的每一个位置都初始化为空状态
//代表相应的位置是未被使用过的
ht->data[i].stat = Empty;
}
}
//销毁
void HashDestroy(HashTable *ht)
{
if(ht == NULL)
{
//非法输入
return;
}
//先将表中的每一个位置都置为无效状态
int i = 0;
for(;i < max_size;i++)
{
ht->data[i].stat = Empty;
}
//再将有效元素个数清0
ht->size = 0;
//哈希函数指向空
ht->func = NULL;
}
//测试一下
void TestInit()
{
Test_Header;
HashTable ht;
HashInit(&ht,Hash_func);
printf("expect size = 0,actual size = %d\n",ht.size);
printf("expect func = %p,actual func = %p\n",Hash_func,ht.func);
}
//插入数据
void HashInsert(HashTable *ht,KeyType key,ValType value)
{
if(ht == NULL)
{
//非法输入
return;
}
//判定当前的hash表能否继续插入
//假设负载因子为0.8
if(ht->size >= 0.8*max_size)
{
//当前hash表已经达到负载因子的上限,不能再继续插入
return;
}
//由key计算offset(由hash函数计算出的存放位置的下标)
int offset = ht->func(key);
//但是该位置可能之前已经被别的数据占据了
//所以我们需要先判断当前计算出的位置是否能放入当前数据
//如果不能就从offset位置往后查找
while(1)
{
//先判断当前计算出的位置是否能放入当前数据
if(ht->data[offset].stat != Valid)
{
//一旦找到一个位置不是有效位置
//就可以将该数据插入
//这就是处理哈希冲突的线性探测法
ht->data[offset].key = key;
ht->data[offset].value = value;
//插入完成以后将该位置置成有效状态
ht->data[offset].stat = Valid;
//哈希表有效元素个数+1
++ht->size;
return;
}
//走到这里说明当前计算出的位置
//不能放置当前待插入的数据
//判断当前位置的元素是否和待插入的元素一样
else if(ht->data[offset].stat == Valid \
&& ht->data[offset].key == key)
{
//说明存在相同元素
//我们这里约定该哈希表中不存在重复元素
//则直接插入失败返回
return;
}
//则更新offset值继续下一次循环往后查找
else
{
++offset;
if(offset >= max_size)
{
//如果查找时offset走到了哈希表的末尾
//还没有找到一个可插入的位置
//则将其置为0,从头开始往后继续查找
offset = 0;
}
}//else结束
}//while结束
}
//打印哈希表中的元素的函数
void HashPrint(HashTable *ht,const char *msg)
{
printf("[%s]\n",msg);
int i = 0;
for(;i < max_size;i++)
{
if(ht->data[i].stat == Valid)
{
printf("(%d:%d,%d) ",i,ht->data[i].key,\
ht->data[i].value);
}
}
printf("\n");
}
//测试一下
void TestInsert()
{
Test_Header;
HashTable ht;
HashInit(&ht,Hash_func);
HashInsert(&ht,1,1);
HashInsert(&ht,1,10);
HashInsert(&ht,2,20);
HashInsert(&ht,1000,100);
HashInsert(&ht,2000,200);
HashPrint(&ht,"插入5个元素");
}
//查找数据
int HashFind(HashTable *ht,KeyType key,ValType *value)
{
if(ht == NULL)
{
//非法输入
return 0;
}
//判断当前hash表中是否有有效元素
if(ht->size == 0)
{
//空哈希表
return 0;
}
//由key值计算出offset
int offset = ht->func(key);
//从offset开始往后查找
while(1)
{
//在当前位置存放的是有效数据的前提下
if(ht->data[offset].stat == Valid)
{
if(ht->data[offset].key == key)
{
//找到了
*value = ht->data[offset].value;
return 1;
}
//当前位置不是待查找的元素
//则更新offset的值继续查找
else
{
++offset;
if(offset >= max_size)
{
offset = 0;
}
}
}
else if(ht->data[offset].stat == Empty)
{
//说明带查找的元素不存在与hash表中
//查找失败返回
return 0;
}
}//while循环结束
return 0;
}
//测试一下
void TestFind()
{
Test_Header;
HashTable ht;
HashInit(&ht,Hash_func);
HashInsert(&ht,1,1);
HashInsert(&ht,1,10);
HashInsert(&ht,2,20);
HashInsert(&ht,1000,100);
HashInsert(&ht,2000,200);
ValType value;
int ret = HashFind(&ht,1000,&value);
printf("查找数据为1000的元素结果为:");
printf("expect ret = 1,actual ret = %d;",ret);
printf("expect value = 100,actual value = %d\n",value);
ret = HashFind(&ht,3000,&value);
printf("查找数据为3000的元素结果为:");
printf("expect ret = 0,actual ret = %d\n",ret);
}
//删除数据
void HashRemove(HashTable *ht,KeyType key)
{
if(ht == NULL)
{
//非法输入
return;
}
if(ht->size == 0)
{
//空哈希表
return;
}
//由key值计算出offset
int offset = ht->func(key);
//从offset开始往后找
while(1)
{
if(ht->data[offset].stat == Valid \
&& ht->data[offset].key == key)
{
//找到了待删除的元素
//直接将该位置的状态置为被删除状态即可
ht->data[offset].stat = Deleted;
//将hash表中有效元素个数-1
--ht->size;
return;
}
else if(ht->data[offset].stat == Empty)
{
//走到这里说明该元素不存在
return;
}
else
{
//走到这里说明当前offset位置的值不是我们想要删除的
//则更新offset值继续查找
++offset;
if(offset >= max_size)
{
offset = 0;
}
}
}//while循环结束
return;
}
//测试一下
void TestRemove()
{
Test_Header;
HashTable ht;
HashInit(&ht,Hash_func);
HashInsert(&ht,1,1);
HashInsert(&ht,1,10);
HashInsert(&ht,2,20);
HashInsert(&ht,1000,100);
HashInsert(&ht,2000,200);
HashRemove(&ht,2);
HashPrint(&ht,"删除数据为2的元素后的结果:");
HashRemove(&ht,20);
HashPrint(&ht,"删除数据为20的元素后的结果:");
}