前面我们提到过哈希冲突产生的原因:
是因为不同关键字通过相同的哈希函数计算得出相同的哈希地址。
那发生了哈希冲突,我们该如何处理呢?
有两种方法:1.闭散列,如线性探测;2.开散列,例如:哈希桶
这里我们用闭散列的方式来解决哈希冲突。
闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被填满,说明哈希表中还存在空余的位置,那么就将key存放到“下一个”空位去。
那如何去找下一个空余的位置呢?
线性探测法,拿我们上次的关键码集合{12,285,609,541,753,456,278,47}为例,散列表的大小为10,假设哈希函数为Hash(key) = key % 10:
当再往表里插入43时,发现了它和753发生了碰撞,那我们就必须要找出下一个存放43的位置。
线性探测的处理方法是:从发生冲突开始,依次继续向后探测,直到找到空位置为止。
所以,我们从下标为3的位置开始往后找,发现它的下一个位置即下标为4为空,那么就将43插入到此位置。
这里,我们需要引入一个负载因子的概念,对于开放地址法来说,是一个特别重要的因素:
a = 填入表中的元素个数 / 散列表的长度
a是散列表装满程度的标志因子。由于表长是定值,a与表中元素个数成正比。所以,a越大,表中元素越多,发生冲突的可能性就越大。
因此,因严格限制a在0.7-0.8以下,超过0.8,查表时的CPU缓存不命中按指数曲线上升。
接下来,我们来对哈希表进行插入、查找和删除操作。
一个哈希表里应包含一个成员数组和哈希函数,而数组的每个元素又是键值对类型,所以它的每一个元素可以定义为一个结构体,对哈希表的定义如下:
#define HashMaxSize 1000
typedef size_t KeyType;
typedef size_t ValType;
typedef enum
{
Empty, //空状态
Deleted, //删除状态
Valid, //有效状态
}Stat;
typedef size_t (*HashFunc)(KeyType key);
//这个结构体代表哈希表中的一个元素
//这个元素中同时也包含了键值对
typedef struct HashElem
{
KeyType key;
ValType value;
Stat stat;
}HashElem;
typedef struct HashTable
{
HashElem data[HashMaxSize];
size_t size;
HashFunc func;//函数指针,指向了hash函数
}HashTable;
插入
1.使用哈希函数找到待插入元素在哈希表中的位置
2.如果该位置没有元素(即状态不是有效的)则直接插入新元素;如果该位置有元素且和待插入元素相等,则不用插入;如果该位置有元素但是和待插入元素不相等,发生哈希冲突,线性探测找到下一个空位置,将元素插入。
代码如下:
void HashInsert(HashTable* ht,KeyType key,ValType value)
{
if(ht == NULL)
{
return;//非法输入
}
//1.判定hash表是否能继续插入(根据负载因子来判断)
//这里我们把负载因子定为0.8
if(ht->size >=0.8 * HashMaxSize)
{
//当前的哈希表已经达到负载因子的上限了
return;//插入失败
}
//2.如果能继续插入,根据key来计算offset
size_t offset = ht->func(key);
//3.从offet位置开始向后线性探测,找到第一个状态为Empty的元素进行插入
while(1)
{
if(ht->data[offset].stat != Valid)
{
//找到了一个合适位置插入元素
ht->data[offset].stat = Valid;
ht->data[offset].key = key;
ht->data[offset].value = value;
//5.++size;
++ht->size;
return;
}
else if(ht->data[offset].key == key && ht->data[offset].stat == Valid)
{
//4.如果发现了key相同的元素,插入失败
return;
}
else
{
++offset;
if(offset >= HashMaxSize)
{
offset = 0;
}
}
}
return;
}
查找
1.根据哈希函数找到要查找元素在哈希表中的位置
2.如果该位置状态为空,说明不存在;如果该位置状态为有效且和待查找元素值一样,说明找到了;如果该位置状态有效但和待查找元素值不一致,继续线性的向后探测,当把哈希表遍历完后还是没找到,说明该元素不存在。
代码:
int HashFind(HashTable* ht,KeyType key,ValType* value)
{
if(ht == NULL || value == NULL)
{
return 0;
}
//1.根据key计算出offse
size_t offset = ht->func(key);
//2.从offset开始向后查找,每找到一个元素,比较其key值和要查找的key是否相同
while(1)
{
if(ht->data[offset].key == key && ht->data[offset].stat == Valid)
{
//3.如果相同,返回其value,查找成功
*value = ht->data[offset].value;
return 1;
}
else if(ht->data[offset].stat == Empty)
{
//4.如果当前是一个空元素,则查找失败
return 0;
}
else
{
//4.如果不相同,继续向后进行查找
offset++;
if(offset >= HashMaxSize)
{
offset = 0;
}
}
}
}
删除
1.根据哈希函数找到要删除元素在哈希表中的位置
2.如果该位置有效且和待删除元素值一样,直接删除;如果该位置状态为空,说明不存在,删除失败;如果该位置有效但和要删除元素值不一致,继续线性的向后探测。
代码:
void HashRemove(HashTable* ht,KeyType key)
{
if(ht == NULL)
{
return;//非法输入
}
if(ht->size == 0)
{
return;//哈希表为空
}
//1.根据key确定offset
size_t offset = ht->func(key);
//2.从offset开始,判断当前元素的key是否和要删除元素的Key值相同
while(1)
{
// a)如果相同,直接删除
if(ht->data[offset].key == key && ht->data[offset].stat == Valid)
{
ht->data[offset].stat = Deleted;
--ht->size;
return;
}
// b)如果当前元素状态为空,说明没找到,直接返回
else if(ht->data[offset].stat == Empty)
{
return;//该元素没有在哈希表中
}
else
{
// c)如果不同,线性地继续向后查找
offset++;
if(offset > HashMaxSize)
{
offset = 0;
}
}
}//循环结束
return;
}