哈希表是一种非常重要的数据结构,它通过一个“哈希函数”将键映射到数组中的一个位置,从而实现高效的插入、删除和查找操作,理想情况下,这些操作的平均时间复杂度可以达到 O(1)。

(图片来源网络,侵删)
下面,我将从以下几个方面来解释:
- 核心概念:哈希表是如何工作的?
- 哈希冲突:以及如何解决它。
- C 语言实现:一个完整的、可运行的哈希表示例。
- 优化与进阶:如何让哈希表更健壮、更高效。
核心概念
哈希表主要由三部分组成:
-
哈希函数:这是一个将“键”转换为数组索引(下标)的函数,一个好的哈希函数应该:
- 计算速度快。
- 分布均匀:将不同的键均匀地散列到数组的各个位置,避免聚集。
- 确定性:同一个键总是产生相同的索引。
在 C 语言中,由于键的类型多种多样(如
int,char*,struct等),我们通常需要一个通用的方法,最简单的方法是将键的内存地址转换为整数,然后对数组大小取模。
(图片来源网络,侵删) -
数组:这是哈希表的“桶”或“槽”,用于存储数据,数组的容量决定了哈希表的大小。
-
键值对:每个槽中存储的数据,包含一个
key和一个value。
基本流程:
- 插入:对
key应用哈希函数,得到索引i,然后将(key, value)存储在数组i的位置。 - 查找:对
key应用哈希函数,得到索引i,然后直接访问数组i的位置,看是否存在该key对应的value。 - 删除:与查找类似,找到
i后,将该位置的数据移除。
哈希冲突
理想情况下,每个键都映射到唯一的索引,但现实是,键的数量可能远大于数组的大小,或者哈希函数不够完美,导致两个不同的键计算出相同的索引,这种情况称为哈希冲突。

(图片来源网络,侵删)
解决哈希冲突主要有两种方法:
A. 链地址法
这是最常用、最容易实现的方法。
- 原理:将哈希表的数组从“存储单个值”变成“存储一个链表的指针”,每个数组元素都是一个链表的头指针。
- 冲突处理:当发生冲突时(即两个键映射到同一个索引),就将新的键值对插入到该索引对应的链表的头部或尾部。
- 优点:实现简单,内存利用率高,当冲突不多时,性能很好。
- 缺点:如果冲突严重,某个链表过长,查找性能会下降到 O(n)。
B. 开放地址法
- 原理:所有键值对都存储在哈希表数组本身中,不使用额外的链表。
- 冲突处理:当发生冲突时,通过一个“探测序列”在数组中寻找下一个“空槽”来插入数据,常见的探测方法有:
- 线性探测:
i = (hash(key) + probe_count) % capacity。hash(key)的位置被占了,就检查hash(key)+1,再不行就检查hash(key)+2,以此类推。 - 二次探测:
i = (hash(key) + probe_count^2) % capacity。
- 线性探测:
- 优点:缓存友好性比链地址法好,因为数据是连续存储的。
- 缺点:容易产生“聚集”现象,即连续的槽被占满,导致查找效率下降,删除操作也比较复杂。
我们将采用更直观、更常用的链地址法来实现。
C 语言实现(链地址法)
下面是一个完整的、带有注释的 C 语言哈希表示例,它支持 int 类型的键和 char* 类型的值。
代码结构
- 定义哈希表节点:每个节点存储一个键值对,以及一个指向下一个节点的指针(用于构建链表)。
- 定义哈希表:包含一个指向节点指针数组的指针,以及哈希表的大小。
- 核心函数:
createHashTable: 创建并初始化哈希表。hashFunction: 哈希函数(这里使用取模法)。insert: 插入键值对。search: 查找键并返回对应的值。delete: 删除一个键值对。freeHashTable: 释放哈希表占用的内存。
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 1. 定义哈希表节点
typedef struct HashNode {
int key; // 键
char* value; // 值
struct HashNode* next; // 指向下一个节点的指针,用于解决冲突
} HashNode;
// 2. 定义哈希表
typedef struct {
int size; // 哈希表的大小(桶的数量)
HashNode** buckets; // 指向桶数组的指针,每个桶是一个链表的头指针
} HashTable;
// 哈希函数:简单的取模法
int hashFunction(int key, int size) {
return abs(key) % size;
}
// 创建一个新的哈希节点
HashNode* createNode(int key, const char* value) {
HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));
if (newNode == NULL) {
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}
newNode->key = key;
// 为值分配内存并复制
newNode->value = strdup(value); // strdup 会分配内存并复制字符串
if (newNode->value == NULL) {
fprintf(stderr, "内存分配失败\n");
free(newNode);
exit(EXIT_FAILURE);
}
newNode->next = NULL;
return newNode;
}
// 创建并初始化哈希表
HashTable* createHashTable(int size) {
HashTable* ht = (HashTable*)malloc(sizeof(HashTable));
if (ht == NULL) {
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}
ht->size = size;
// 为桶数组分配内存
ht->buckets = (HashNode**)calloc(size, sizeof(HashNode*));
if (ht->buckets == NULL) {
fprintf(stderr, "内存分配失败\n");
free(ht);
exit(EXIT_FAILURE);
}
return ht;
}
// 向哈希表中插入键值对
void insert(HashTable* ht, int key, const char* value) {
int index = hashFunction(key, ht->size);
HashNode* newNode = createNode(key, value);
// 情况1:桶为空,直接插入
if (ht->buckets[index] == NULL) {
ht->buckets[index] = newNode;
}
// 情况2:桶不为空,发生冲突,插入到链表头部
else {
newNode->next = ht->buckets[index];
ht->buckets[index] = newNode;
}
printf("插入成功: Key = %d, Value = %s\n", key, value);
}
// 在哈希表中搜索键
const char* search(HashTable* ht, int key) {
int index = hashFunction(key, ht->size);
HashNode* current = ht->buckets[index];
// 遍历链表以查找键
while (current != NULL) {
if (current->key == key) {
return current->value; // 找到,返回值
}
current = current->next;
}
return NULL; // 未找到
}
// 从哈希表中删除键值对
void delete(HashTable* ht, int key) {
int index = hashFunction(key, ht->size);
HashNode* current = ht->buckets[index];
HashNode* prev = NULL;
// 遍历链表查找要删除的节点
while (current != NULL && current->key != key) {
prev = current;
current = current->next;
}
// 情况1:未找到
if (current == NULL) {
printf("删除失败: Key %d 不存在\n", key);
return;
}
// 情况2:找到了
// 如果是链表的第一个节点
if (prev == NULL) {
ht->buckets[index] = current->next;
} else { // 如果是中间或最后一个节点
prev->next = current->next;
}
// 释放节点的内存
free(current->value); // 先释放值
free(current); // 再释放节点本身
printf("删除成功: Key = %d\n", key);
}
// 释放哈希表的所有内存
void freeHashTable(HashTable* ht) {
for (int i = 0; i < ht->size; i++) {
HashNode* current = ht->buckets[i];
while (current != NULL) {
HashNode* temp = current;
current = current->next;
free(temp->value);
free(temp);
}
}
free(ht->buckets);
free(ht);
}
int main() {
// 创建一个大小为10的哈希表
HashTable* ht = createHashTable(10);
// 插入一些数据
insert(ht, 1, "Apple");
insert(ht, 2, "Banana");
insert(ht, 12, "Cherry"); // 12 % 10 = 2, 与 2 冲突
insert(ht, 22, "Date"); // 22 % 10 = 2, 与 2, 12 冲突
// 查找数据
printf("\n--- 查找测试 ---\n");
const char* val1 = search(ht, 1);
printf("Key 1 的值: %s\n", val1 ? val1 : "未找到");
const char* val12 = search(ht, 12);
printf("Key 12 的值: %s\n", val12 ? val12 : "未找到");
const char* val99 = search(ht, 99);
printf("Key 99 的值: %s\n", val99 ? val99 : "未找到");
// 删除数据
printf("\n--- 删除测试 ---\n");
delete(ht, 2);
delete(ht, 99); // 尝试删除不存在的键
// 再次查找以验证删除
printf("\n--- 删除后查找 ---\n");
val1 = search(ht, 1);
printf("Key 1 的值: %s\n", val1 ? val1 : "未找到");
val12 = search(ht, 12);
printf("Key 12 的值: %s\n", val12 ? val12 : "未找到");
// 释放哈希表
freeHashTable(ht);
return 0;
}
如何编译和运行
- 将上述代码保存为
hashtable.c。 - 使用 gcc 进行编译:
gcc hashtable.c -o hashtable
- 运行程序:
./hashtable
预期输出
插入成功: Key = 1, Value = Apple
插入成功: Key = 2, Value = Banana
插入成功: Key = 12, Value = Cherry
插入成功: Key = 22, Value = Date
--- 查找测试 ---
Key 1 的值: Apple
Key 12 的值: Cherry
Key 99 的值: 未找到
--- 删除测试 ---
删除成功: Key = 2
删除失败: Key 99 不存在
--- 删除后查找 ---
Key 1 的值: Apple
Key 12 的值: Cherry
优化与进阶
这个基础的实现已经展示了哈希表的核心思想,但在实际应用中,我们还需要考虑更多:
-
动态扩容:
- 问题:当哈希表中的元素数量越来越多,冲突的概率也会急剧增加,导致链表变长,性能下降。
- 解决方案:设置一个“负载因子”(
Load Factor = 元素数量 / 哈希表大小),当负载因子超过某个阈值(如 0.75)时,创建一个更大的新哈希表(通常是原来的两倍),然后将所有旧元素重新哈希并插入到新表中,这是一个复杂但至关重要的优化。
-
更复杂的哈希函数:
- 对于
int键,简单的取模法就足够了。 - 对于
char*(字符串) 键,可以使用更复杂的算法,如 DJB2 或 FNV-1,这些算法能更好地将字符串的字符分布到哈希值中。
- 对于
-
更通用的键类型:
- 目前的实现只支持
int键,要支持struct或其他自定义类型,需要用户自定义一个“哈希函数”和一个“比较函数”,并将其作为参数传递给哈希表,C++ 的模板和函数对象(Functor)就是为了解决这个问题而设计的。
- 目前的实现只支持
-
更完善的错误处理:
- 在
search函数中,如果键不存在,返回NULL可能会有歧义(因为value也可能是NULL),更好的做法是返回一个指向节点的指针,让调用者自己判断node->value,或者使用一个特殊的结构体来封装查找结果。
- 在
-
使用开放地址法:
可以尝试实现开放地址法(特别是线性探测),并比较它与链地址法在缓存性能和内存占用上的差异。
希望这份详细的解释和代码示例能帮助你完全理解 C 语言中的哈希表!
