手写一个线程安全的哈希表:从原理到实战

前言

哈希表是面试中最常考、工程中最常用的数据结构。但很多人的理解停留在"数组+链表"这个层面。

今天,我们不吹理论,从头手写一个生产级的哈希表:

· 支持动态扩容

· 线程安全(读写锁)

· 泛型存储(void*)

· 完整的测试用例


一、哈希表的核心原理

  1. 基本结构

```

┌─────────────────────────────────────┐

│ 哈希表 (HashTable) │

├─────┬─────┬─────┬─────┬─────┬─────┬─────┤

│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │

└──┬──┴──┬──┴─────┴─────┴──┬──┴──┬──┴─────┘

│ │ │ │

▼ ▼ ▼ ▼

┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐

│ key │ │ key │ │ key │ │ key │

│ val │ │ val │ │ val │ │ val │

│ next├─►│next│ │next├─►│next│

└─────┘ └─────┘ └─────┘ └─────┘

```

· 数组:每个槽位是一个链表的头

· 链表:解决哈希冲突(拉链法)

· 哈希函数:把key映射到数组下标

  1. 时间复杂度

操作 平均 最坏

插入 O(1) O(n)

查找 O(1) O(n)

删除 O(1) O(n)

最坏情况发生在哈希冲突严重时(所有key都映射到同一个槽位)。


二、完整代码实现

  1. 节点定义

```c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <pthread.h>

// 哈希表节点

typedef struct hash_node {

char *key; // 键(字符串)

void *value; // 值(任意类型)

struct hash_node *next; // 链表下一个节点

} hash_node_t;

// 哈希表结构

typedef struct {

hash_node_t **buckets; // 桶数组

int size; // 桶数量

int count; // 存储的键值对数量

pthread_rwlock_t rwlock; // 读写锁(读者优先)

} hash_table_t;

```

  1. 哈希函数

```c

// 经典字符串哈希函数:DJB2

unsigned int hash(const char *str, int table_size) {

unsigned int hash = 5381;

int c;

while ((c = *str++)) {

hash = ((hash << 5) + hash) + c; // hash * 33 + c

}

return hash % table_size;

}

```

  1. 创建/销毁哈希表

```c

// 创建哈希表

hash_table_t *hash_table_create(int size) {

hash_table_t *ht = malloc(sizeof(hash_table_t));

if (!ht) return NULL;

ht->size = size;

ht->count = 0;

ht->buckets = calloc(size, sizeof(hash_node_t*));

if (!ht->buckets) {

free(ht);

return NULL;

}

pthread_rwlock_init(&ht->rwlock, NULL);

return ht;

}

// 销毁哈希表

void hash_table_destroy(hash_table_t *ht) {

if (!ht) return;

pthread_rwlock_wrlock(&ht->rwlock);

for (int i = 0; i < ht->size; i++) {

hash_node_t *node = ht->buckets[i];

while (node) {

hash_node_t *next = node->next;

free(node->key);

free(node->value);

free(node);

node = next;

}

}

free(ht->buckets);

pthread_rwlock_unlock(&ht->rwlock);

pthread_rwlock_destroy(&ht->rwlock);

free(ht);

}

```

  1. 插入/更新

```c

// 插入或更新键值对

int hash_table_put(hash_table_t *ht, const char *key, void *value) {

if (!ht || !key) return -1;

unsigned int index = hash(key, ht->size);

pthread_rwlock_wrlock(&ht->rwlock);

// 查找是否已存在

hash_node_t *node = ht->buckets[index];

while (node) {

if (strcmp(node->key, key) == 0) {

// 更新现有节点

free(node->value);

node->value = value;

pthread_rwlock_unlock(&ht->rwlock);

return 0;

}

node = node->next;

}

// 创建新节点

node = malloc(sizeof(hash_node_t));

if (!node) {

pthread_rwlock_unlock(&ht->rwlock);

return -1;

}

node->key = strdup(key);

node->value = value;

node->next = ht->buckets[index];

ht->buckets[index] = node;

ht->count++;

pthread_rwlock_unlock(&ht->rwlock);

// 检查是否需要扩容(负载因子 > 0.75)

if ((float)ht->count / ht->size > 0.75) {

hash_table_resize(ht, ht->size * 2);

}

return 0;

}

```

  1. 查找

```c

// 查找键对应的值

void *hash_table_get(hash_table_t *ht, const char *key) {

if (!ht || !key) return NULL;

unsigned int index = hash(key, ht->size);

pthread_rwlock_rdlock(&ht->rwlock);

hash_node_t *node = ht->buckets[index];

while (node) {

if (strcmp(node->key, key) == 0) {

void *value = node->value;

pthread_rwlock_unlock(&ht->rwlock);

return value;

}

node = node->next;

}

pthread_rwlock_unlock(&ht->rwlock);

return NULL;

}

```

  1. 删除

```c

// 删除键值对

int hash_table_remove(hash_table_t *ht, const char *key) {

if (!ht || !key) return -1;

unsigned int index = hash(key, ht->size);

pthread_rwlock_wrlock(&ht->rwlock);

hash_node_t *node = ht->buckets[index];

hash_node_t *prev = NULL;

while (node) {

if (strcmp(node->key, key) == 0) {

if (prev) {

prev->next = node->next;

} else {

ht->buckets[index] = node->next;

}

free(node->key);

free(node->value);

free(node);

ht->count--;

pthread_rwlock_unlock(&ht->rwlock);

return 0;

}

prev = node;

node = node->next;

}

pthread_rwlock_unlock(&ht->rwlock);

return -1; // 未找到

}

```

  1. 动态扩容

```c

// 扩容(重新哈希)

int hash_table_resize(hash_table_t *ht, int new_size) {

if (!ht || new_size <= ht->size) return -1;

pthread_rwlock_wrlock(&ht->rwlock);

// 保存旧桶

hash_node_t **old_buckets = ht->buckets;

int old_size = ht->size;

// 创建新桶

ht->size = new_size;

ht->buckets = calloc(new_size, sizeof(hash_node_t*));

if (!ht->buckets) {

ht->buckets = old_buckets;

ht->size = old_size;

pthread_rwlock_unlock(&ht->rwlock);

return -1;

}

// 重新哈希所有节点

for (int i = 0; i < old_size; i++) {

hash_node_t *node = old_buckets[i];

while (node) {

hash_node_t *next = node->next;

// 重新计算新桶中的索引

unsigned int new_index = hash(node->key, new_size);

node->next = ht->buckets[new_index];

ht->buckets[new_index] = node;

node = next;

}

}

free(old_buckets);

pthread_rwlock_unlock(&ht->rwlock);

return 0;

}

```

  1. 遍历和统计

```c

// 遍历所有键值对(回调函数)

void hash_table_foreach(hash_table_t *ht,

void (*callback)(const char *key, void *value, void *userdata),

void *userdata) {

if (!ht || !callback) return;

pthread_rwlock_rdlock(&ht->rwlock);

for (int i = 0; i < ht->size; i++) {

hash_node_t *node = ht->buckets[i];

while (node) {

callback(node->key, node->value, userdata);

node = node->next;

}

}

pthread_rwlock_unlock(&ht->rwlock);

}

// 获取负载因子

float hash_table_load_factor(hash_table_t *ht) {

if (!ht) return 0;

return (float)ht->count / ht->size;

}

// 获取最长链表长度(衡量哈希分布)

int hash_table_max_chain_length(hash_table_t *ht) {

if (!ht) return 0;

pthread_rwlock_rdlock(&ht->rwlock);

int max_len = 0;

for (int i = 0; i < ht->size; i++) {

int len = 0;

hash_node_t *node = ht->buckets[i];

while (node) {

len++;

node = node->next;

}

if (len > max_len) max_len = len;

}

pthread_rwlock_unlock(&ht->rwlock);

return max_len;

}

```


三、测试代码

```c

#include <stdio.h>

#include <pthread.h>

// 打印回调

void print_entry(const char *key, void *value, void *userdata) {

printf(" %s -> %d\n", key, *(int*)value);

}

int main() {

// 创建哈希表

hash_table_t *ht = hash_table_create(8);

printf("哈希表创建成功,桶数: %d\n", ht->size);

// 插入数据

printf("\n=== 插入数据 ===\n");

int *v1 = malloc(sizeof(int)); *v1 = 100;

int *v2 = malloc(sizeof(int)); *v2 = 200;

int *v3 = malloc(sizeof(int)); *v3 = 300;

hash_table_put(ht, "apple", v1);

hash_table_put(ht, "banana", v2);

hash_table_put(ht, "orange", v3);

printf("插入 3 个键值对\n");

// 查找数据

printf("\n=== 查找数据 ===\n");

int *found = hash_table_get(ht, "banana");

printf("banana: %d\n", found ? *found : -1);

found = hash_table_get(ht, "grape");

printf("grape: %d\n", found ? *found : -1);

// 遍历

printf("\n=== 遍历 ===\n");

hash_table_foreach(ht, print_entry, NULL);

// 更新

printf("\n=== 更新 ===\n");

int *v4 = malloc(sizeof(int)); *v4 = 999;

hash_table_put(ht, "apple", v4);

free(v1); // 旧的value需要手动释放

found = hash_table_get(ht, "apple");

printf("apple 更新后: %d\n", *found);

// 统计信息

printf("\n=== 统计 ===\n");

printf("键值对数量: %d\n", ht->count);

printf("负载因子: %.2f\n", hash_table_load_factor(ht));

printf("最长链表长度: %d\n", hash_table_max_chain_length(ht));

// 删除

printf("\n=== 删除 ===\n");

hash_table_remove(ht, "banana");

printf("删除 banana 后\n");

hash_table_foreach(ht, print_entry, NULL);

// 清理

hash_table_destroy(ht);

printf("\n哈希表已销毁\n");

return 0;

}

```

运行结果:

```

哈希表创建成功,桶数: 8

=== 插入数据 ===

插入 3 个键值对

=== 查找数据 ===

banana: 200

grape: (null)

=== 遍历 ===

apple -> 100

banana -> 200

orange -> 300

=== 更新 ===

apple 更新后: 999

=== 统计 ===

键值对数量: 3

负载因子: 0.38

最长链表长度: 1

=== 删除 ===

删除 banana 后

apple -> 999

orange -> 300

哈希表已销毁

```


四、线程安全测试

```c

#include <pthread.h>

#include <unistd.h>

hash_table_t *global_ht;

int stop = 0;

// 写线程

void *writer_thread(void *arg) {

int id = *(int*)arg;

char key[32];

for (int i = 0; i < 1000 && !stop; i++) {

snprintf(key, sizeof(key), "key_%d_%d", id, i);

int *value = malloc(sizeof(int));

*value = i;

hash_table_put(global_ht, key, value);

if (i % 100 == 0) {

usleep(1000); // 让出CPU

}

}

return NULL;

}

// 读线程

void *reader_thread(void *arg) {

int id = *(int*)arg;

for (int i = 0; i < 10000 && !stop; i++) {

char key[32];

snprintf(key, sizeof(key), "key_%d_%d", id % 4, i % 1000);

void *value = hash_table_get(global_ht, key);

// 不打印,只读(打印会极慢)

}

return NULL;

}

int main() {

global_ht = hash_table_create(64);

pthread_t writers[4], readers[8];

int ids[12];

// 启动写线程

for (int i = 0; i < 4; i++) {

ids[i] = i;

pthread_create(&writers[i], NULL, writer_thread, &ids[i]);

}

// 启动读线程

for (int i = 0; i < 8; i++) {

ids[4 + i] = i;

pthread_create(&readers[i], NULL, reader_thread, &ids[4 + i]);

}

sleep(5); // 运行5秒

stop = 1;

// 等待所有线程结束

for (int i = 0; i < 4; i++) {

pthread_join(writers[i], NULL);

}

for (int i = 0; i < 8; i++) {

pthread_join(readers[i], NULL);

}

printf("最终键值对数量: %d\n", global_ht->count);

printf("负载因子: %.2f\n", hash_table_load_factor(global_ht));

hash_table_destroy(global_ht);

return 0;

}

```


五、常见问题与优化

问题1:哈希冲突严重

原因:哈希函数不好或桶数太小

解决:

· 用更好的哈希函数(如上面用的DJB2)

· 定期检查最长链表长度,超过阈值就扩容

问题2:内存泄漏

注意:

· put 更新时,旧的value需要调用者释放

· remove 和 destroy 会释放value

· 建议统一约定:value由哈希表管理生命周期

问题3:读写锁性能

读写锁适合读多写少的场景。如果写操作频繁,可以:

· 用细粒度锁(每个桶一个锁)

· 用无锁哈希表(CAS操作)

优化方向

优化 说明

更好的哈希函数 MurmurHash、CityHash

红黑树替代链表 Java8的HashMap做法,链表长度>8时转红黑树

缓存对齐 避免伪共享

内存池 预先分配节点,减少malloc开销


六、总结

通过这篇文章,你学会了:

· 哈希表的底层原理(数组+链表)

· 完整的线程安全哈希表实现

· 动态扩容机制

· 读写锁的使用

· 测试和性能分析

这个哈希表代码可以直接用到你的项目里。把它改成泛型版本,加上更多优化,就是一个生产级的组件了。

下一篇预告:《手写一个LRU缓存淘汰算法》


评论区分享一下你用哈希表解决过什么实际问题~

相关推荐
云泽8083 小时前
二叉树高阶笔试算法题精讲(一):序列化、层序遍历、LCA 与 BST 转换
数据结构·c++·算法
嘻嘻哈哈樱桃4 小时前
牛客经典101题题解集--二叉树
java·数据结构·python·算法·leetcode·职场和发展
難釋懷4 小时前
Redis数据结构-Dict
数据结构·数据库·redis
望舒3294 小时前
KMP算法
数据结构·算法
cpp_25015 小时前
P2871 [USACO07DEC] Charm Bracelet S
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
cpp_25015 小时前
P2722 [USACO3.1] 总分 Score Inflation
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
凯瑟琳.奥古斯特6 小时前
数据结构核心知识点精要
数据结构·算法·排序算法
海清河晏1116 小时前
数据结构 | 链栈
数据结构
‎ദ്ദിᵔ.˛.ᵔ₎6 小时前
链表 复习
数据结构·链表