哈希表(Hash Table)是一种高效的数据结构,用于实现快速的数据查找、插入和删除操作。哈希表通过将关键字(Key)映射到表中的位置(索引),实现近似常数时间的操作效率。哈希表在许多应用中广泛使用,如数据库索引、缓存系统、编译器符号表等。本文将详细介绍如何使用C语言实现哈希表,包括基本概念、哈希函数、冲突处理方法、基本操作、示例代码及其优缺点。
哈希表是一种通过哈希函数将关键字映射到数组索引的数据结构。每个元素(键值对)由一个键(Key)和一个值(Value)组成。理想情况下,哈希函数能将不同的键均匀地分布到哈希表的各个位置,从而实现高效的数据操作。
哈希函数是哈希表的核心,用于将键转换为数组索引。一个好的哈希函数应具备以下特性:
确定性:相同的键每次映射的索引应相同。
均匀性:键应均匀分布到哈希表的各个位置,尽量减少冲突。
高效性:计算哈希值的时间复杂度应尽可能低。
常见的哈希函数包括除留余数法、乘法散列法、字符串哈希函数等。
由于哈希函数将有限的索引映射到可能无限的键集合,冲突是不可避免的。常见的冲突处理方法包括:
链地址法(Separate Chaining):每个哈希表的索引对应一个链表,当多个键映射到同一索引时,将它们存储在该索引的链表中。
开放地址法(Open Addressing):当发生冲突时,通过探测方法(如线性探测、二次探测、双重哈希等)寻找下一个可用的位置存储键值对。
本教程将采用链地址法来实现哈希表,因为其实现相对简单且适用于动态数据集。
数据库索引:快速查找记录。
缓存系统:实现高效的缓存查找。
编译器符号表:存储变量和函数的信息。
密码学:用于数据加密和哈希验证。
字典和集合的实现。
在C语言中,哈希表通常由一个数组和一组链表(用于链地址法)组成。每个数组元素称为桶(Bucket),每个桶存储一个或多个键值对。
首先,定义哈希表节点(键值对)的结构体:
#include
#include
#include
// 定义键值对的结构体
typedef struct HashNode {
char* key; // 键
int value; // 值
struct HashNode* next; // 指向下一个节点的指针(用于链地址法)
} HashNode;
接下来,定义哈希表的结构体:
// 定义哈希表的结构体
typedef struct HashTable {
int size; // 哈希表的大小(桶的数量)
HashNode** table; // 指向哈希表数组的指针
} HashTable;
为了将键(字符串)映射到哈希表的索引,我们需要设计一个有效的哈希函数。以下是一个简单的哈希函数示例,采用了djbx33a算法(又称为djb2哈希函数),它对字符串具有较好的分布性:
// djb2哈希函数
unsigned int hashFunction(char* key, int size) {
unsigned long hash = 5381;
int c;
while ((c = *key++))
hash = ((hash << 5) + hash) + c; // hash * 33 + c
return hash % size;
}
创建一个指定大小的哈希表,并初始化所有桶为空:
// 创建并初始化哈希表
HashTable* createHashTable(int size) {
HashTable* hashtable = (HashTable*)malloc(sizeof(HashTable));
if (!hashtable) {
printf("内存分配失败\n");
exit(1);
}
hashtable->size = size;
hashtable->table = (HashNode**)malloc(sizeof(HashNode*) * size);
if (!hashtable->table) {
printf("内存分配失败\n");
exit(1);
}
for (int i = 0; i < size; i++)
hashtable->table[i] = NULL;
return hashtable;
}
创建一个新的哈希表节点:
// 创建一个新的哈希表节点
HashNode* createHashNode(char* key, int value) {
HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));
if (!newNode) {
printf("内存分配失败\n");
exit(1);
}
newNode->key = strdup(key); // 复制键字符串
newNode->value = value;
newNode->next = NULL;
return newNode;
}
将一个键值对插入到哈希表中。如果键已存在,则更新其值:
// 插入键值对到哈希表
void insert(HashTable* hashtable, char* key, int value) {
unsigned int index = hashFunction(key, hashtable->size);
HashNode* current = hashtable->table[index];
// 检查键是否已存在
while (current) {
if (strcmp(current->key, key) == 0) {
current->value = value; // 更新值
return;
}
current = current->next;
}
// 如果键不存在,则创建新节点并插入链表头部
HashNode* newNode = createHashNode(key, value);
newNode->next = hashtable->table[index];
hashtable->table[index] = newNode;
}
根据键查找对应的值:
// 查找键对应的值,若找到则返回值,否则返回-1
int search(HashTable* hashtable, char* key) {
unsigned int index = hashFunction(key, hashtable->size);
HashNode* current = hashtable->table[index];
while (current) {
if (strcmp(current->key, key) == 0)
return current->value;
current = current->next;
}
return -1; // 表示未找到
}
根据键删除对应的键值对:
// 根据键删除键值对
void deleteKey(HashTable* hashtable, char* key) {
unsigned int index = hashFunction(key, hashtable->size);
HashNode* current = hashtable->table[index];
HashNode* prev = NULL;
while (current) {
if (strcmp(current->key, key) == 0) {
if (prev)
prev->next = current->next;
else
hashtable->table[index] = current->next;
free(current->key);
free(current);
return;
}
prev = current;
current = current->next;
}
printf("键 '%s' 未找到,无法删除。\n", key);
}
遍历并打印哈希表中的所有键值对:
// 打印哈希表
void printHashTable(HashTable* hashtable) {
for (int i = 0; i < hashtable->size; i++) {
HashNode* current = hashtable->table[i];
printf("桶 %d:", i);
while (current) {
printf(" -> [键: %s, 值: %d]", current->key, current->value);
current = current->next;
}
printf("\n");
}
}
释放哈希表占用的内存:
// 销毁哈希表,释放内存
void destroyHashTable(HashTable* hashtable) {
for (int i = 0; i < hashtable->size; i++) {
HashNode* current = hashtable->table[i];
while (current) {
HashNode* temp = current;
current = current->next;
free(temp->key);
free(temp);
}
}
free(hashtable->table);
free(hashtable);
}
以下是一个完整的哈希表实现示例程序,展示了如何插入、查找、删除和打印哈希表中的键值对:
#include
#include
#include
// 定义键值对的结构体
typedef struct HashNode {
char* key;
int value;
struct HashNode* next;
} HashNode;
// 定义哈希表的结构体
typedef struct HashTable {
int size;
HashNode** table;
} HashTable;
// djb2哈希函数
unsigned int hashFunction(char* key, int size) {
unsigned long hash = 5381;
int c;
while ((c = *key++))
hash = ((hash << 5) + hash) + c; // hash * 33 + c
return hash % size;
}
// 创建并初始化哈希表
HashTable* createHashTable(int size) {
HashTable* hashtable = (HashTable*)malloc(sizeof(HashTable));
if (!hashtable) {
printf("内存分配失败\n");
exit(1);
}
hashtable->size = size;
hashtable->table = (HashNode**)malloc(sizeof(HashNode*) * size);
if (!hashtable->table) {
printf("内存分配失败\n");
exit(1);
}
for (int i = 0; i < size; i++)
hashtable->table[i] = NULL;
return hashtable;
}
// 创建一个新的哈希表节点
HashNode* createHashNode(char* key, int value) {
HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));
if (!newNode) {
printf("内存分配失败\n");
exit(1);
}
newNode->key = strdup(key);
newNode->value = value;
newNode->next = NULL;
return newNode;
}
// 插入键值对到哈希表
void insert(HashTable* hashtable, char* key, int value) {
unsigned int index = hashFunction(key, hashtable->size);
HashNode* current = hashtable->table[index];
// 检查键是否已存在
while (current) {
if (strcmp(current->key, key) == 0) {
current->value = value; // 更新值
return;
}
current = current->next;
}
// 如果键不存在,则创建新节点并插入链表头部
HashNode* newNode = createHashNode(key, value);
newNode->next = hashtable->table[index];
hashtable->table[index] = newNode;
}
// 查找键对应的值,若找到则返回值,否则返回-1
int search(HashTable* hashtable, char* key) {
unsigned int index = hashFunction(key, hashtable->size);
HashNode* current = hashtable->table[index];
while (current) {
if (strcmp(current->key, key) == 0)
return current->value;
current = current->next;
}
return -1; // 表示未找到
}
// 根据键删除键值对
void deleteKey(HashTable* hashtable, char* key) {
unsigned int index = hashFunction(key, hashtable->size);
HashNode* current = hashtable->table[index];
HashNode* prev = NULL;
while (current) {
if (strcmp(current->key, key) == 0) {
if (prev)
prev->next = current->next;
else
hashtable->table[index] = current->next;
free(current->key);
free(current);
return;
}
prev = current;
current = current->next;
}
printf("键 '%s' 未找到,无法删除。\n", key);
}
// 打印哈希表
void printHashTable(HashTable* hashtable) {
for (int i = 0; i < hashtable->size; i++) {
HashNode* current = hashtable->table[i];
printf("桶 %d:", i);
while (current) {
printf(" -> [键: %s, 值: %d]", current->key, current->value);
current = current->next;
}
printf("\n");
}
}
// 销毁哈希表,释放内存
void destroyHashTable(HashTable* hashtable) {
for (int i = 0; i < hashtable->size; i++) {
HashNode* current = hashtable->table[i];
while (current) {
HashNode* temp = current;
current = current->next;
free(temp->key);
free(temp);
}
}
free(hashtable->table);
free(hashtable);
}
int main() {
// 创建一个大小为10的哈希表
HashTable* hashtable = createHashTable(10);
// 插入键值对
insert(hashtable, "apple", 100);
insert(hashtable, "banana", 200);
insert(hashtable, "orange", 300);
insert(hashtable, "grape", 400);
insert(hashtable, "melon", 500);
// 打印哈希表
printf("初始哈希表:\n");
printHashTable(hashtable);
// 查找键
char* keyToFind = "banana";
int value = search(hashtable, keyToFind);
if (value != -1)
printf("键 '%s' 的值是 %d\n", keyToFind, value);
else
printf("键 '%s' 未找到\n", keyToFind);
// 删除键
char* keyToDelete = "orange";
deleteKey(hashtable, keyToDelete);
printf("删除键 '%s' 后的哈希表:\n", keyToDelete);
printHashTable(hashtable);
// 更新键
insert(hashtable, "apple", 150);
printf("更新键 'apple' 后的哈希表:\n");
printHashTable(hashtable);
// 销毁哈希表
destroyHashTable(hashtable);
return 0;
}
初始哈希表:
桶 0:
桶 1: -> [键: apple, 值: 100]
桶 2:
桶 3:
桶 4:
桶 5: -> [键: banana, 值: 200]
桶 6: -> [键: orange, 值: 300]
桶 7: -> [键: grape, 值: 400]
桶 8:
桶 9: -> [键: melon, 值: 500]
键 'banana' 的值是 200
删除键 'orange' 后的哈希表:
桶 0:
桶 1: -> [键: apple, 值: 100]
桶 2:
桶 3:
桶 4:
桶 5: -> [键: banana, 值: 200]
桶 6:
桶 7: -> [键: grape, 值: 400]
桶 8:
桶 9: -> [键: melon, 值: 500]
更新键 'apple' 后的哈希表:
桶 0:
桶 1: -> [键: apple, 值: 150]
桶 2:
桶 3:
桶 4:
桶 5: -> [键: banana, 值: 200]
桶 6:
桶 7: -> [键: grape, 值: 400]
桶 8:
桶 9: -> [键: melon, 值: 500]
高效的查找、插入和删除:哈希表的平均时间复杂度为O(1),适用于需要频繁操作的数据集。
灵活的键类型:支持多种键类型(如字符串、整数等),通过适当的哈希函数实现。
动态扩展:哈希表的大小可以根据需要动态调整,以适应不同的数据规模。
冲突处理开销:虽然大多数情况下操作效率高,但在冲突频繁的情况下,性能可能下降。
哈希函数设计:需要设计高效且均匀的哈希函数,避免冲突过多。
空间浪费:为了减少冲突,哈希表通常需要预留一定的空桶,可能导致空间浪费。
不支持有序操作:哈希表不维护键的顺序,不适合需要有序数据的场景。
数据库索引:用于快速定位记录。
编译器符号表:存储变量、函数等符号的信息。
缓存系统:实现快速的数据缓存,如内存缓存、网页缓存。
唯一性检测:用于检查数据集合中的重复元素。
字典和集合的实现:实现高效的键值对存储和查询。
随着数据量的增加,哈希表的负载因子(Load Factor,即元素数量与哈希表大小的比值)可能增大,导致冲突增加。为了保持高效性,通常需要在负载因子达到某一阈值时,扩展哈希表的大小,并重新哈希所有元素。
选择适合具体键类型的哈希函数,可以显著减少冲突,提高哈希表的性能。例如,对于字符串键,可以使用更复杂的哈希函数如MurmurHash、SipHash等。
根据具体应用场景,选择适合的冲突处理策略。例如,链地址法适用于动态数据集,而开放地址法在内存使用上更为紧凑。