在计算机世界中,哈希表如同一位聪慧的图书管理员。他知道如何计算索书号,从而可以快速找到目标图书。

1.哈希表的概念

哈希表(hash table),又称散列表,它通过建立键key 与值value 之间的映射,实现高效的元素查询。具体而言,我们向哈希表中输入一个键key ,则可以在𝑂(1) 时间内获取对应的值value 。

1.1哈希表的基本操作

  • 添加元素
  • 查询元素
  • 删除元素
    在哈希表中进行增删查改的时间复杂度都是𝑂(1),非常高效。

1.2哈希表的常用操作

哈希表的常见操作包括:初始化、查询操作、添加键值对和删除键值对等。

2.基于数实现哈希表

2.1哈希表的结构体定义

首先,我们将key 和value 封装成一个类Pair ,以表示键值对。

//键值对
typedef struct{
    int key;
    int *val;
}Pair;

其次,我们来定义哈希表的结构体

// 基于数组实现哈希表
typedef struct{
    Pair* buckes[Max_Size];
}ArrayHashMap;

2.2哈希表的初始化

首先,为哈希表动态分配内存空间,其次,将哈希表中的每个槽(bucket)初始化为空或NULL,表示没有元素存储在这些槽中,如果初始化成功,返回哈希表指针.

// 哈希表的初始化
ArrayHashMap *InitHashMap(){
    //为哈希表分配内存
    ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));
    if(hmap == NULL){
        printf("内存分配失败!\n");
        return -1;
    }
    //将每一个键值对置空
    for (int i = 0; i < Max_Size; i++){
        hmap->buckets[i] = NULL;
    }
    return hmap; 
}

2.3删除哈希表

首先,遍历一遍哈希表,将哈希表的键值对释放,再将哈希表的槽释放,最后将哈希表释放。

// 哈希表的删除
void DesttoryHashMap(ArrayHashMap *hmap){
    for (int i = 0; i < Max_Size; i++)
    {
        if(hmap->buckets[i] != NULL){
            free(hmap->buckets[i]->val);
            free(hmap->buckets[i]);
        }
    }
    free(hmap);
}

2.4哈希函数

哈希函数的作用是将一个较大的输入空间映射到一个较小的输出空间。在哈希表中,输入空间是所有key ,输出空间是所有桶(数组索引)。换句话说,输入一个key ,我们可以通过哈希函数得到该key 对应的键值对在数组中的存储位置。

//哈希函数
unsigned int HashFunction(int key){
    return abs(key) % Max_Size;
}

2.5查找哈希表中的元素

在哈希表中查找给定键的值。如果找到,返回对应的值;否则返回 -1。

// 哈希表的查找
int *SearchInHashMap(ArrayHashMap *hamp, int key){
    unsigned int index = HashFunction(key);
    if(hamp->buckets[index]->key == key){
        return hamp->buckets[index]->val;
    }
    return -1;  //没找到
}

2.6 删除哈希表中的元素

// 删除哈希表的元素
bool DeleteInHashMap(ArrayHashMap *hmap, int key){
    unsigned int index = HashFunction(key);
    if(hmap->buckets[index]->key == key){
        hmap->buckets[index]->key = NULL;
        return true;
    }
    return false;
}

2.7添加哈希表元素

将新的键值对添加到哈希表中。如果槽位已被占用,这里我不解决哈希冲突,则返回 false,表示添加失败

// 添加哈希表函数
bool AddHashMap(ArrayHashMap *hmap, int key, int val){
    unsigned int index = HashFunction(key);
    if(hmap->buckets[index]->key == key){
        //这里我们不解决哈希冲突
        printf("槽位已经被占用!\n");
        return false;
    }
    hmap->buckets[index]->key = key;
    hmap->buckets[index]->val = val;
    return true;
}

3.哈希冲突与扩容

什么是哈希冲突?
从本质上看,哈希函数的作用是将所有key 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,理论上一定存在 “多个输入对应相同输出” 的情况。我们将这称之为哈希冲突。
容易想到,哈希表容量𝑛 越大,多个key 被分配到同一个桶中的概率就越低,冲突就越少。因此,我们可以通过扩容哈希表来减少哈希冲突。
类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,非常耗时;并且由于哈希表容量capacity 改变,我们需要通过哈希函数来重新计算所有键值对的存储位置,这进一步增加了扩容过程的计算开销。为此,编程语言通常会预留足够大的哈希表容量,防止频繁扩容。

4.链式地址改良哈希表

哈希冲突会导致查询结果错误,严重影响哈希表的可用性。为了解决该问题,每当遇到哈希冲突时,我们就进行哈希表扩容,直至冲突消失为止。此方法简单粗暴且有效,但效率太低,因为哈希表扩容需要进行大量的数据搬运与哈希值计算。为了提升效率,我们可以通过链式存储结构来改良哈希表。
基于链式存储结构哈希表发生了以下变化:

  • 查询元素:输入key ,经过哈希函数得到桶索引,即可访问链表头节点,然后遍历链表并对比key 以查找目标键值对。
  • 添加元素:首先通过哈希函数访问链表头节点,然后将节点(键值对)添加到链表中。
  • 删除元素:根据哈希函数的结果访问链表头部,接着遍历链表以查找目标节点并将其删除。

4.1链式地址哈希表结构体定义

//键值对
typedef struct
{
    int key;
    int *val;
} Pair;

//链表节点 
typedef struct Node{
    Pair* pair;
    struct Node* next;
}Node;

//哈希表节点定义
typedef struct {
    int size;   //键值对的数量
    int capcity;    //哈希表的容量
    double loadThres;   //触发扩容的负载因子阈值
    int extendRatio;    //扩容倍数
    Node **buckets;     //桶数组
}HashMapChaining;

4.2哈希表的初始化

//哈希表初始化
HashMapChaining *InitHashMap(){
    HashMapChaining* hashmap = (HashMapChaining*)malloc(sizeof(HashMapChaining));
    hashmap->size = 0;
    hashmap->capcity = 4;
    hashmap->loadThres = 2.0 / 3.0;
    hashmap->extendRatio = 2;
    hashmap->buckets = (Node**)malloc(sizeof(Node*)*hashmap->capcity);
    for (int i = 0; i < hashmap->capcity; i++)
    {
        hashmap->buckets[i] = NULL;
    }
    return hashmap;
    
}

4.3 哈希表的销毁

//哈希表的销毁
void DesrtroyHashMap(HashMapChaining *hashmap){
    for (int  i = 0; i < hashmap->capcity; i++){
        Node* cur = hashmap->buckets[i];
        while (cur){
            Node* temp = cur;
            cur = cur->next;
            free(temp->pair);
            free(temp);
        }
    }
    free(hashmap->buckets);
    free(hashmap);
}

4.4哈希函数

//哈希函数
int HashFuntion(HashMapChaining *hashmap, int key){
    return key % hashmap->capcity;
}

4.5 负载因子

double loadFactor(HashMapChaining *hashMap){
    return (double)hashMap->size / (double)hashMap->capcity;
}

4.6哈希表的扩容

/* 扩容哈希表*/
void extend(HashMapChaining *hashMap)
{
    // 暂存原哈希表
    int oldCapacity = hashMap->capcity;
    Node **oldBuckets = hashMap->buckets;
    // 初始化扩容后的新哈希表
    hashMap->capcity *= hashMap->extendRatio;
    hashMap->buckets = (Node **)malloc(hashMap->capcity * sizeof(Node *));
    for (int i = 0; i < hashMap->capcity; i++)
    {
        hashMap->buckets[i] = NULL;
    }
    hashMap->size = 0;
    // 将键值对从原哈希表搬运至新哈希表
    for (int i = 0; i < oldCapacity; i++)
    {
        Node *cur = oldBuckets[i];
        while (cur)
        {
            put(hashMap, cur->pair->key, cur->pair->val);
            Node *temp = cur;
            cur = cur->next;
            // 释放内存
            free(temp->pair);
            free(temp);
        }
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐