数据结构 --- 开散列处理哈希冲突(哈希桶)

开散列

什么是开散列?

开散列法又叫链地址法(开链法)。
首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于统一子集合,每一个子集合称为一个桶,各个桶中的元素通过单链表连接起来,各链表的头结点存储在哈希表中。

假如一个元素集合的关键码为{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,而表项所占空间又比指针大得多,所以使用链地址法反而比开地址法节省存储空间。

没有更多推荐了,返回首页