开散列
什么是开散列?
开散列法又叫链地址法(开链法)。
首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于统一子集合,每一个子集合称为一个桶,各个桶中的元素通过单链表连接起来,各链表的头结点存储在哈希表中。
假如一个元素集合的关键码为{37,25,14,36,49,68,57,11},散列表为HT[12],表的大小为12,散列函数为Hash(x) = x % 11
先根据哈希函数计算出每个元素所在的桶号
然后将元素插入到哈希表中
同一个桶的链表中存放哈希冲突的元素。
操作
接下来,用开散列方法实现哈希表的插入,查找和删除操作。
先定义一个哈希表并对其初始化:
#define HashMaxSize 1000
typedef size_t KeyType;
typedef size_t ValType;
typedef size_t (*HashFunc)(KeyType key);
typedef struct HashElem
{
KeyType key;
ValType value;
struct HashElem* next;
}HashElem;
typedef struct HashTable
{
//我们这里定义的哈希桶上的链表是一个不带头结点的链表
HashElem* data[HashMaxSize];
size_t size;
HashFunc func;
}HashTable;
//哈希函数
size_t HashFuncDefault(KeyType key)
{
return key % HashMaxSize;
}
//创建数组成员
HashElem* CreateElem(KeyType key,ValType value)
{
HashElem* new_node = (HashElem*)malloc(sizeof(HashElem));
new_node->key = key;
new_node->value = value;
new_node->next = NULL;
return new_node;
}
void DestroyElem(HashElem* node)
{
free(node);
return;
}
void HashInit(HashTable* ht,HashFunc func)
{
if(ht == NULL)
{
return;//非法输入
}
ht->size = 0;
ht->func = func;
int i = 0;
for( ; i < HashMaxSize;i++)
{
ht->data[i] = NULL;
}
return;
}
//销毁哈希表
void HashDestroy(HashTable* ht)
{
if(ht == NULL)
{
return;
}
ht->size = 0;
ht->func = NULL;
//遍历整个哈希表,释放元素
size_t i = 0;
for( ;i < HashMaxSize;i++)
{
HashElem* cur = ht->data[i];
while(cur != NULL)
{
HashElem* next = cur->next;
DestroyElem(cur);
cur = next;
}
}
return;
}
插入
1.先根据哈希函数算出哈希地址offset
2.然后在哈希表的offset对应的链表中查找该元素是否存在
3. 如果存在,插入失败;如果不存在,采用头插的方式将该元素插入到链表中。
void HashInsert(HashTable* ht,KeyType key,ValType value)
{
if(ht == NULL)
{
return;//非法输入
}
//1.先根据key计算出offset
size_t offset = ht->func(key);
//2.在offset对应的链表里查找当前key是否存在
HashElem* ret = HashBucketFind(ht->data[offset],key);
if(ret != NULL)
{
// a)如果存在,插入失败
return;
}
// b)如果不存在,直接插入,采用头插
HashElem* new_node = CreateElem(key,value);
new_node->next = ht->data[offset];
ht->data[offset] = new_node;
++ht->size;
return;
}
//哈希桶查找,在一个链表里查找该元素是否存在,遍历整个链表
HashElem* HashBucketFind(HashElem* head,KeyType to_find)
{
HashElem* cur = head;
while(cur != NULL)
{
if(cur->key == to_find)
{
break;
}
cur = cur->next;
}
return cur != NULL ? cur:NULL;
}
查找
借助我们实现的哈希桶查找函数,即在一个链表里查找该元素是否存在
1.根据哈希函数算出offset
2.然后在offset对应的链表里查找该元素是否存在(HashBucketFind()函数)
int HashFind(HashTable* ht,KeyType key,ValType* value)
{
if(ht == NULL)
{
return 0;//非法输入
}
//1.先找到要查找元素的下标
size_t offset = ht->func(key);
//2.根据下标找到对应的链表
// 在链表中查找该值是否存在
HashElem* ret = HashBucketFind(ht->data[offset],key);
if(ret == NULL)
{
return 0;
}
*value = ret->value;
return 1;
}
删除
我们的哈希表是基于链表的,删除哈希表中的一个元素实质上是对链表进行操作,一个不带环不带头结点的单链表在进行删除的时候,我们必须通过遍历的方式找到它的前一个结点,才能对它进行删除操作。
因此,这里我们需要修改我们的哈希桶查找函数。
//哈希桶查找,此时我们需要知道要删除元素的前一个结点
int HashBucketFindEx(HashElem* head,KeyType to_remove,HashElem** pre_node,HashElem** cur_node)
{
if(head == NULL)
{
return 0;
}
HashElem* pre = NULL;
HashElem* cur = head;
while(cur != NULL)
{
if(to_remove == cur->key)
{
*pre_node = pre;
*cur_node = cur;
return 1;
}
pre = cur;
cur = cur->next;
}
return 0;
}
1.先根据哈希函数算出哈希地址offset
2.在offset对应的链表里查找该元素是否存在
3.如果不存在,删除失败;如果存在,删除该元素
void HashRemove(HashTable* ht,KeyType to_remove)
{
if(ht == NULL)
{
return;
}
//1.先找到要删除元素的下标
size_t offset = ht->func(to_remove);
//2.查找该下标对应的链表中是否存在该元素
HashElem* cur = NULL;
HashElem* pre = NULL;
int ret = HashBucketFindEx(ht->data[offset],to_remove,&pre,&cur);
if(ret == 0)
{
return;//没找到
}
if(pre == NULL)
{
//要删除的元素是链表头结点
ht->data[offset] = cur->next;
}
else
{//要删除的元素不是头结点
pre->next = cur->next;
}
DestroyElem(cur);
--ht->size;
return;
}
通常,每个桶对应的链表结点都很少,将n个关键码通过某一个散列函数,存放到散列表的m个桶中,那么每一个桶中链表的平均长度为 n / m,以搜索平均长度为 n / m的链表代替了长度为 n 的顺序表,搜索效率快得多。
应用链地址法处理冲突,需要增设链接指针,似乎增加了存储开销。事实上:
由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探测法要求装载因子 a <= 0.7,而表项所占空间又比指针大得多,所以使用链地址法反而比开地址法节省存储空间。