哈希表

哈希表是常用的数据结构,在理想情况下,查找和删除元素的复杂度为O(1)。在自己实现一个简单的哈希表的过程中,也加深对哈希表原理上理解,把一个个抽象的概念具体化。

实际上我们可能已经接触过简单的哈希表,考虑下面这个例子:

统计一个段落中,小写英文字母的个数。

我们需要遍历完整个段落,用容器存放每个字母的个数,伪代码如下:

vector<int> container;
for (char ch : paragraph) {
    container[ch - 'a']++;
}

我们可以把container作为一个哈希表,ch-'a'是哈希表的key,value则是每个字母的个数。

1 哈希函数

前面我们发现数组可以作为哈希表的底层容器。英文字母中每个字母都是独一无二,而且只涉及到单个字母,场景简单,如果key是长度不一的字符串呢?假设哈希表的底层容器数组,我们需要一个函数,通过这个函数我们计算出来一个数值,根据这个数值我们得到数组的索引。我们对于这个函数的要求是,尽最大可能每个长度不一的字符串计算出来的值是不同的。这个函数就是哈希函数。哈希函数有很多种计算方法,而不是一种唯一的计算方法。本文使用的是FNV-1a哈希算法

#define FNV_PRIME 16777619
#define OFFSET_BASIS 2166136261

/**
 * @brief use FNV-1a to calculate hash value
 * 
 * @param value 
 * @param len 
 * @return * unsigned int hash value
 */
unsigned int hash(char* value, unsigned int len)
{
    unsigned int i;
    unsigned int hash_val = OFFSET_BASIS;

    if (value == NULL) {
        LOG_ERR("Input param is null!");
    }

    for (i = 0; i < len; ++i) {
        hash_val ^= value[i];
        hash_val *= FNV_PRIME;
    }
    return hash_val;
}

字符串的哈希值是尽可能唯一的,如果哈希表底层数组足够大,那么我们可以把字符串的哈希值作为数组的索引,那么就可以找到存放的位置。但是实际上数组是不可能无限大的,因此采用取余作为索引:

unsigned int hash_val = hash((char*)(key->key), key->key_len);
unsigned long bucket_idx = hash_val % array_size; // array_size是数组长度

2 哈希碰撞

尽管哈希函数使计算的哈希值不重复,但是实际上总是会有重复的,并且由于数组的容量是有限的,取余计算后就会计算得到相同的数组索引,这种情况称之为哈希碰撞。也就是两个不同的字符串计算得到的数组下表相同。解决哈希碰撞常用的方法有4种,本文使用的是链地址法。哈希表和桶的结构体定义如下:

struct bucket
{
    struct key_field key;
    struct value_field value;
    unsigned int hash_val;
    struct bucket* next;
};

struct hash_map
{
    unsigned long capacity;
    unsigned long buckets_cnt; 
    float load_factor;
    unsigned long size;
    struct bucket* buckets;
};

当哈希表插入数据时,如果哈希表中没有对应的key,就会插入到桶对应的链表下。struct bucket中的next指针就是用来挂链表的。

    prev_target = find_key_node_before(bucket, *key);
    if (prev_target->next) {
        copy_field((unsigned char*)(&(prev_target->next->value)), (unsigned char*)(&val));
    } else {
        prev_target->next = malloc(sizeof(struct bucket));
        memset(prev_target->next, 0, sizeof(struct bucket));
        copy_field((unsigned char*)(&(prev_target->next->value)), (unsigned char*)(&val));
        copy_field((unsigned char*)(&prev_target->next->key), (unsigned char*)(&key));
    }

3 rehash

当我们不停往哈希表中插入数据,到一定程度之后,再插入数据就会提高哈希碰撞的概率。这个时候我们就需要对哈希表进行扩容。那么如何评估当前需要进行rehash的操作呢?

装载因子的值是哈希表容量与已存放元素之比。在理想情况下,一个元素计算完哈希要插入哈希表时,不存在冲突的情况,哈希表的桶都是用来存放元素。但是实际上不可能不冲突,这里采用的是链地址法来解决冲突。当哈希表存放到一定容量后,再插入元素就需要rehash的操作。装载因子就是这个警戒线。rehash需要对数组进行扩容,但是我们要遵循什么样的原则进行扩容呢?本文才用和C++的unordered_map相同的策略,找到第一个大于当前数组长度的素数作为新的数组长度。

const unsigned long __prime_list[] = // 256 + 1 or 256 + 48 + 1
{
    2ul, 3ul, 5ul, 7ul, 11ul, 13ul, 17ul, 19ul, 23ul, 29ul, 31ul,
    37ul, 41ul, 43ul, 47ul, 53ul, 59ul, 61ul, 67ul, 71ul, 73ul, 79ul,
    83ul, 89ul, 97ul, 103ul, 109ul, 113ul, 127ul, 137ul, 139ul, 149ul,
    157ul, 167ul, 179ul, 193ul, 199ul, 211ul, 227ul, 241ul, 257ul,
    277ul, 293ul, 313ul, 337ul, 359ul, 383ul, 409ul, 439ul, 467ul,
    503ul, 541ul, 577ul, 619ul, 661ul, 709ul, 761ul, 823ul, 887ul,
    953ul, 1031ul, 1109ul, 1193ul, 1289ul, 1381ul, 1493ul, 1613ul,
    ....
}

__prime_list是个已排序素数表,当前只展示一部分。可以通过二分法查找到第一个大于当前数组长度的素数。


static struct hash_map* rehash(hash_map* map)
{
    struct bucket* tmp;
    unsigned long new_bucket_cnt, old_bucket_cnt;
    unsigned long rehash_size;
    unsigned long idx;
    unsigned int hash_value;
    struct bucket* old_buckets = map->buckets;
    struct bucket* prev, *p;
    old_bucket_cnt = map->buckets_cnt;
    new_bucket_cnt = lower_bound(__prime_list, sizeof(__prime_list) / sizeof(unsigned long), 100);
    rehash_size = new_bucket_cnt * map->load_factor;

    tmp = malloc(rehash_size * sizeof(struct bucket));
    if (tmp == NULL) {
        LOG_ERR("malloc memory for resizing hash map failed.");
        return NULL;
    }

    memset(tmp, 0, sizeof(rehash_size * sizeof(struct bucket)));
    map->buckets_cnt = new_bucket_cnt;
    map->capacity = rehash_size;
    map->buckets = tmp;
    /* 遍历整个哈希表,把哈希表的内容放到新的哈希表中 */
    for (idx = 0; idx < old_bucket_cnt; ++idx) {
        prev = &old_buckets[idx];
        while(prev->next) {
            insert_element(map, &(prev->next->key), &(prev->next->value));
            p = prev->next;
            prev = p->next;
            release_bucket(p);
            p = NULL;
            if (!prev) {
                break;
            }
        }
    }
    free(old_buckets);
    return map;
}

4 插入操作

本文的哈希表在插入元素的时候,是直接挂载桶的链表下面的。在插入元素之前,需要确定哈希表中是否存在要插入的元素,如果有就更新对应的节点,否则就生成一个新的节点挂载链表最后:

static void insert_element(hash_map* map, key_field* key, val_field* val)
{
    struct bucket* prev_target;
    unsigned int hash_val = hash((char*)(key->key), key->key_len);
    unsigned long bucket_idx;
    struct bucket* bucket;

    bucket_idx = hash_val % map->buckets_cnt;
    bucket = &map->buckets[bucket_idx];
    /* 查找是否存在要插入的元素节点 */
    prev_target = find_key_node_before(bucket, *key);
    if (prev_target->next) {
        copy_field((unsigned char*)(&(prev_target->next->value)), (unsigned char*)(&val));
    } else {
        prev_target->next = malloc(sizeof(struct bucket));
        memset(prev_target->next, 0, sizeof(struct bucket));
        copy_field((unsigned char*)(&(prev_target->next->value)), (unsigned char*)(&val));
        copy_field((unsigned char*)(&prev_target->next->key), (unsigned char*)(&key));
    }

}

在查找的时候使用了一个小技巧,我们去找要插入的元素的前一个节点。这样做的好处是,能够提高在插入或删除操作的效率。因为在删除链表节点时需要知道前一个链表节点。这个也是C++的unordered_map实现中使用的技巧。

static struct bucket* find_key_node_before(struct bucket* p_prev, key_field key)
{
    struct bucket* p, *tmp_prev;
    if (!p_prev) {
        return NULL;
    }

    tmp_prev = p_prev;
    for (p = tmp_prev->next; p; p = p->next) {
        if (p->key.key_type != key.key_type ||
            p->key.key_len != key.key_len ||
            memcmp((void*)(p->key.key), (void*)(key.key), key.key_len)) {
                tmp_prev = p;
                continue;
        } else {
            break;
        }

    }

    return tmp_prev;
}

5 删除操作

删除操作和插入操作类似,找到待删除节点的前一个节点,然后删除节点并释放内存:

bool hashmap_delete(hash_map* map, key_field key)
{
    if (map == NULL) {
        return false;
    }

    unsigned int hash_val = hash((char*)(key.key), key.key_len);
    unsigned long bucket_idx;
    struct bucket* bucket, *del_bucket;
    struct bucket* prev_target;

    bucket_idx = hash_val % map->buckets_cnt;
    bucket = &map->buckets[bucket_idx];
    prev_target = find_key_node_before(bucket, key);
    if (prev_target->next) {
        del_bucket = prev_target->next;
        prev_target->next = del_bucket->next;
        release_bucket(del_bucket); /* 释放桶节点的内存 */
    } else {
        return false;
    }

    return true;
}
posted @ 2026-01-26 16:06  cockpunctual  阅读(1)  评论(0)    收藏  举报