闭散列法处理哈希冲突
通常,我们所谓的哈希冲突可以定义为对于两个不相等的数据元素 i和 j,将他们代入哈希函数hashFunc有:
hashFunc(i) == hashFunc(j)
即不同关键字通过相同哈希函数计算出相同的哈希地址,对于哈希冲突的处理方法,通常有两种:闭散列法和开散列法,本文先介绍闭散列法,也叫开放地址法。当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把关键字key存放到表中“下一个位置“。
首先定义一个哈希表结构体HashTable,结构体里面包含储存关键字key集合的数组table,数组元素的个数size,数组的容量capacity和所设计的哈希函数hashFunc。采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。这里将数组table中的每个位置当前状况采用标一种枚举类型的state来标注,state分为三种状态:删除DELETED,存在EXIST和空白EMPTY。同时,将数组table定义为一种包含关键字key和枚举state的Element结构体类型。相关代码如下:
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int Key;
typedef enum
{
EMPTY,
EXIST,
DELETED
}State;
typedef struct Element{
Key key;
State state;
}Element;
typedef int ( *HashFuncType)(Key key, int capacity);
typedef struct HashTable
{
Element *table;
int size;//数据个数
int capacity;//容量
HashFuncType hashFunc;//哈希函数
}HashTable;
对于哈希表结构体的初始化和销毁,代码如下:
//初始化
void HashInit(HashTable *pHT, int capacity, HashFuncType hashFunc)
{
pHT->table = (Element *)malloc(sizeof(Element)*capacity);
assert(pHT->table);
pHT->size = 0;
pHT->capacity = capacity;
pHT->hashFunc = hashFunc;
for (int i = 0; i < capacity; i++)
{
pHT->table[i].state = EMPTY;
}
}
//销毁
void Destory(HashTable *pHT)
{
free(pHT->table);
}
这里对于哈希函数的定义采用除留余数法,代码如下:
int hashFunc(Key key, int capacity)
{
return key%capacity;
}
当发生哈希冲突时,如果哈希表未被装满,把key存放到表中“下一个” 空位中去 ,对于寻找下一个空余位置,有两种方法,首先介绍第一种方法——一次线性探测法,当插入时,从发生冲突的位置开始,依次继续向后探测,如果该位置中有元素且和待插入元素key相同,则不用插入;如果该位置中有元素但不是待插入元素则发生哈希冲突,使用线性探测找到下一个空位置,插入新元素key。采用一次线性探测,发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”。解决办法把数组的容量capacity扩容为原来的2倍。扩充的前提条件是散列表的负载因子不小于7。负载因子可定义为
即pHT->size*10/pHT->capacity<7。
一次线性探测插入的代码如下:
//插入
//key重复不允许插入
void ExpandIfRequired(HashTable *pHT)
{
//不扩容
if (pHT->size*10/pHT->capacity<7)
{
return;
}
//需要扩容
int newCapacity = pHT->capacity * 2;
HashTable newHT;
HashInit(&newHT, newCapacity, pHT->hashFunc);
//将旧数组中的数据搬移到新数组中
for (int i = 0; i < pHT->capacity; i++)
{
if (pHT->table[i].state == EXIST)
{
HashInsert(&newHT, pHT->table[i].key);
}
}
free(pHT->table);
pHT->table = newHT.table;
pHT->capacity = newHT.capacity;
}
int HashInsert(HashTable *pHT, Key key)
{
ExpandIfRequired(pHT);
int index = pHT->hashFunc(key, pHT->capacity);
while (1)
{
if (pHT->table[index].key == key
&&pHT->table[index].state == EXIST)
{
return 0;
}
if (pHT->table[index].state != EXIST)
{
pHT->table[index].key = key;
pHT->table[index].state = EXIST;
pHT->size++;
return 1;
}
index = (index + 1) % pHT->capacity;
}
return 0;
}
对于插入我的测试代码如下:
void test()
{
HashTable ht;
HashInit(&ht, 10, hashFunc);
HashInsert(&ht, 1);
HashInsert(&ht, 11);
HashInsert(&ht, 21);
HashInsert(&ht, 31);
HashInsert(&ht, 41);
HashInsert(&ht, 51);
HashInsert(&ht, 61);
HashInsert(&ht, 71);
HashInsert(&ht, 81);
Destory(&ht);
}
下面是测试行结果
对于关键字的查找,如果找到关键字返回其所在位置下标,否则返回0,代码如下:
//查找
int HashSearch1(HashTable *pHT, Key key)
{
int index = pHT->hashFunc(key, pHT->capacity);
while ((pHT->table[index].state) != EMPTY)
{
if (pHT->table[index].key == key&&pHT->table[index].state == EXIST)
{
return index;
}
else
index = (index + 1) % pHT->capacity;
}
//没找到
return 0;
}
对于关键字的删除,当查找到要删除的关键字时,只需将这个位置的状态置为删除DELETED。如果删除失败,则返回0。代码如下:
void HashTableRemove(HashTable *pHT, Key key)
{
int index = pHT->hashFunc(key, pHT->capacity);
while ((pHT->table[index].state) !=EMPTY)
{
if (pHT->table[index].key == key&&pHT->table[index].state == EXIST)
{
pHT->table[index].state = DELETED;
return 1;
}
else
index = (index + 1) % pHT->capacity;
}
return 0;
}
接下来的第二种方法是二次探测法,二次探测法相对于一次探测法,优点在于解决了一次探测数据“堆积”的问题。。二次探测法的下一个空位计算方式为index = (oindex + i*i) % pHT->capacity,其中i代表查找次数,oindex代表上一找到的位置。这里只叙述二次探测法查找代码,其代码如下:
int HashSearch2(HashTable *pHT, Key key)
{
int oindex = pHT->hashFunc(key, pHT->capacity);//i-1次查找的的元素下标
int index = oindex;
int i = 1;//i代表查找次数
while ((pHT->table[index].state) != EMPTY)
{
if (pHT->table[index].key == key&&pHT->table[index].state == EXIST)
{
return index;
}
//二次探测法变化的位置
index = (oindex + i*i) % pHT->capacity;
i++;
}
//没找到
return 0;
}