超适合初学者——哈希表的C语言简单实现
文章目录
哈希表
1.为什么需要哈希表?
从一个实际问题开始,比如在10万条学生记录中,如何通过学号快速找到某个学生的信息?
- 如果用数组,需要遍历,O(n)时间复杂度,太慢
- 如果用链表,同样需要遍历
- 如果学号直接作为数组下标呢?但如果学号是字符串(如”S20240001“)或者范围很大(如0-10亿),直接作为下标会浪费巨大内存
这时,我们需要一种结构,既能像数组一样通过“下标”快速访问,又能灵活处理各种类型的键并节省空间。这就是哈希表
2. 哈希表的核心思想
- 核心概念: 将任意长度的键(Key)通过一个哈希函数 映射到一个固定范围的数组下标。
- 工作流程:
- 存储(Put):
键(Key) -> 哈希函数 -> 数组索引(Index) -> 在索引处存储值(Value) - 查找(Get):
键(Key) -> 哈希函数 -> 数组索引(Index) -> 从索引处读取值(Value)
- 存储(Put):
- 哈希表的要素:
| 要素 | 作用(解释) |
|---|---|
| 键(key) | 节点编号 |
| 值(value) | 节点信息 |
| 索引(index) | 数组的下标,用以快速定位和检索数据 |
| 哈希桶 | 保存索引的数组(链表),数组成员为每一个索引值相同的多个元素 |
| 哈希函数 | 将一个较大的输入空间映射到一个较小的输出空间,即将节点编号映射到索引上,采用求余法 |
3. 哈希冲突
- 什么是哈希冲突?
两个不同的键,经过哈希函数计算后,得到了相同的数组索引,这就是哈希冲突
- 解决方案:
- 链地址法:
- 思想:数组的每一个元素不是一个值,而是一个链表(或其他结构)的头指针。所有映射到同一索引的键值对都放在这个链表里
- 优缺点:
- 优点:简单有效,易于实现
- 缺点:需要额外的指针存储空间
- 开放地址法:
- 思想:当发生冲突时,按照某种探测序列在哈希表中寻找下一个空闲位置
- 探测方法:
- 线性探测:
index = (hash(key) + i) % table_size - 二次探测:
index = (hash(key) + i²) % table_size - 双重散列:
index = (hash1(key) + i * hash2(key)) % table_size
- 线性探测:
- 优缺点:
- 优点:所以数据都存储在数组中
- 缺点:删除操作复杂
- 链地址法:
4. C语言实现简单哈希表
4.1 定义
#include<stdio.h>#include<stdlib.h>#include<math.h>//定义哈希表节点typedefstructHashNode{int key;//键int value;//值structHashNode* next;//下一节点}HashNode;//定义哈希表typedefstruct{ HashNode** table;//存储哈希表中各个桶的头指针(指针的指针)int size;//哈希表大小int count;//元素个数}HashTable;4.2 创建哈希表
//创建哈希表 HashTable*createHashTable(int size){if(size <=0){printf("哈希表大小必须大于0\n");returnNULL;} HashTable* hashTable =(HashTable*)malloc(sizeof(HashTable));if(hashTable ==NULL){printf("内存分配失败\n");returnNULL;} hashTable->size = size; hashTable->count =0;//初始化元素数量 hashTable->table =(HashNode**)malloc(sizeof(HashNode*)* size);if(hashTable->table ==NULL){printf("内存分配失败\n");free(hashTable);returnNULL;}//初始化所有桶为空for(int i =0; i < size; i++){ hashTable->table[i]=NULL;}printf("创建哈希表成功,大小为:%d\n",size);return hashTable;}代码说明:
- 函数声明,返回
HashTable指针,参数size为哈希表大小,即哈希桶的个数 - 分配
HashTable结构体的内存空间,初始化哈希表:size:设置哈希表的大小(哈希桶的数量)count:当前哈希表内的元素数量初始化为0
- 再为哈希桶分配内存(每个桶都是一个
HashNode*的指针):遍历所有桶,将每个桶都初始化为NULL - 最后,返回创建好的哈希表指针
4.3 哈希函数
//哈希函数inthashFunction(HashTable* hashTable,int key){returnabs(key)% hashTable->size;}代码说明:
- 计算并返回哈希值
- 取
key的绝对值,避免负数导致计算出的哈希值为负数(数组索引不能为负数) - 取模:将
key映射到[0,hashTable->size - 1]的范围内,确保哈希值在哈希表的有效索引范围内
- 取
4.4 插入哈希表
//插入哈希表intinsertHash(HashTable* hashTable,int key,int value){if(hashTable ==NULL){printf("哈希表为空,无法插入\n");return0;//插入失败}int hashIndex =hashFunction(hashTable, key);//检查键是否已经存在 HashNode* curNode = hashTable->table[hashIndex];while(curNode !=NULL){if(curNode->key == key){ curNode->value = value;printf("更新键:%d\n", key);return2;//更新操作} curNode = curNode->next;}//创建新节点 HashNode* newNode =(HashNode*)malloc(sizeof(HashNode));if(newNode ==NULL){printf("内存分配失败\n");return0;} newNode->key = key; newNode->value = value;//插入到链表头部 newNode->next = hashTable->table[hashIndex]; hashTable->table[hashIndex]= newNode; hashTable->count++;return1;//插入成功}代码说明:
- 首先,检查哈希表指针是否为空,若为空,则插入失败,返回0
- 接着,计算哈希索引:
- 调用前面的哈希函数
HashFunction,根据key计算应该存储在哪个桶里 - 检查键是否存在:
- 获取对应桶的链表头指针
- 遍历链表
- 若找到相同的
key:更新value值,返回2,表示更新;若未找到,则说明key不存在,需要插入新节点
- 调用前面的哈希函数
- 创建新节点,设置新节点的键值对,再将其插入链表头部(头插法)
- 最后增加哈希表的元素个数,返回1,表示插入成功
4.5 查找哈希表
//查找哈希表intsearchHash(HashTable* hashTable,int key,int* value){if(hashTable ==NULL|| value ==NULL){return0;}int hashIndex =hashFunction(hashTable, key); HashNode* curNode = hashTable->table[hashIndex];while(curNode !=NULL){if(curNode->key == key){*value = curNode->value;return1;//找到} curNode = curNode->next;}return0;//没找到}代码说明:
- 该函数中的参数分别为:哈希表指针
HashTable* hashTable、要查找的目标键int key、输出参数,用来存储找到的值int* value - 检查哈希表指针和输出参数指针是否为空
- 接着,计算哈希索引:
- 调用
hashFunction函数 - 计算
key对应的桶索引 - 获取对应桶的链表头指针,直接访问哈希表的桶数组
- 调用
- 遍历链表,找到匹配的
key,将值赋给输出参数*value
4.6 删除哈希表
//删除哈希表intDeleteHash(HashTable* hashTable,int key){if(hashTable ==NULL){printf("哈希表为空,无法删除\n");return0;//删除失败}int hashIndex =hashFunction(hashTable, key); HashNode* curNode = hashTable->table[hashIndex]; HashNode* prevNode =NULL;while(curNode !=NULL){if(curNode->key == key){if(prevNode ==NULL){//删除头节点 hashTable->table[hashIndex]= curNode->next;}else{//删除中间或尾部节点 prevNode->next = curNode->next;}free(curNode); hashTable->count--;printf("删除成功\n");return1;//删除成功} prevNode = curNode; curNode = curNode->next;}printf("删除失败\n");return0;//未找到键,删除失败}代码说明:
- 检查哈希表指针是否为空
- 计算哈希索引:
- 调用
hashFunction函数 - 计算key对应的桶索引
- 调用
- 遍历链表查找目标键:
curNode:当前节点指针,初始化为链表头;prevNode:前驱节点指针,初始化为NULL- 循环直到链表末尾
- 检查当前节点的key是否匹配目标key
- 找到后,进行删除操作:
- 删除头节点(
prevNode == NULL)- 将桶的头指针指向当前节点的下一个节点
- 直接跳过要删除的节点
- 删除中间或尾部节点
- 将前驱节点的next指针指向当前节点的下一个节点
- 跳过要删除的节点
- 删除:
free(curNode):释放被删除节点的内存hashTable->count--:减少哈希表元素个数
- 删除头节点(
4.7 释放哈希表
//释放哈希表voidfreeHash(HashTable* hashTable){if(hashTable ==NULL){return;}for(int i =0; i < hashTable->size; i++){ HashNode* curNode = hashTable->table[i];while(curNode !=NULL){ HashNode* temp = curNode; curNode = curNode->next;free(temp);}}free(hashTable->table);free(hashTable);}代码说明:
- 检查哈希表指针是否为空
- 遍历所有桶----->遍历当前桶的链表----->释放所有节点
- 释放桶数组内存
- 释放哈希表结构体内存
4.8 完整测试
#include<stdio.h>#include<stdlib.h>#include<math.h>//定义哈希表节点typedefstructHashNode{int key;//键int value;//值structHashNode* next;//下一节点}HashNode;//定义哈希表typedefstruct{ HashNode** table;//存储哈希表中各个桶的头指针(指针的指针)int size;//哈希表大小int count;//元素个数}HashTable;//创建哈希表 HashTable*createHashTable(int size){if(size <=0){printf("哈希表大小必须大于0\n");returnNULL;} HashTable* hashTable =(HashTable*)malloc(sizeof(HashTable));if(hashTable ==NULL){printf("内存分配失败\n");returnNULL;} hashTable->size = size; hashTable->count =0;//初始化元素数量 hashTable->table =(HashNode**)malloc(sizeof(HashNode*)* size);if(hashTable->table ==NULL){printf("内存分配失败\n");free(hashTable);returnNULL;}//初始化所有桶为空for(int i =0; i < size; i++){ hashTable->table[i]=NULL;}printf("创建哈希表成功,大小为:%d\n",size);return hashTable;}//哈希函数inthashFunction(HashTable* hashTable,int key){returnabs(key)% hashTable->size;}//插入哈希表intinsertHash(HashTable* hashTable,int key,int value){if(hashTable ==NULL){printf("哈希表为空,无法插入\n");return0;//插入失败}int hashIndex =hashFunction(hashTable, key);//检查键是否已经存在 HashNode* curNode = hashTable->table[hashIndex];while(curNode !=NULL){if(curNode->key == key){ curNode->value = value;printf("更新键:%d\n", key);return2;//更新操作} curNode = curNode->next;}//创建新节点 HashNode* newNode =(HashNode*)malloc(sizeof(HashNode));if(newNode ==NULL){printf("内存分配失败\n");return0;} newNode->key = key; newNode->value = value;//插入到链表头部 newNode->next = hashTable->table[hashIndex]; hashTable->table[hashIndex]= newNode; hashTable->count++;printf("插入成功: 键=%d, 值=%d\n", key, value);return1;//插入成功}//查找哈希表intsearchHash(HashTable* hashTable,int key,int* value){if(hashTable ==NULL|| value ==NULL){return0;}int hashIndex =hashFunction(hashTable, key); HashNode* curNode = hashTable->table[hashIndex];while(curNode !=NULL){if(curNode->key == key){*value = curNode->value;return1;//找到} curNode = curNode->next;}return0;//没找到}//删除哈希表中的节点intDeleteHash(HashTable* hashTable,int key){if(hashTable ==NULL){printf("哈希表为空,无法删除\n");return0;//删除失败}int hashIndex =hashFunction(hashTable, key); HashNode* curNode = hashTable->table[hashIndex]; HashNode* prevNode =NULL;while(curNode !=NULL){if(curNode->key == key){if(prevNode ==NULL){//删除头节点 hashTable->table[hashIndex]= curNode->next;}else{//删除中间或尾部节点 prevNode->next = curNode->next;}free(curNode); hashTable->count--;printf("删除键 %d 成功\n", key);return1;//删除成功} prevNode = curNode; curNode = curNode->next;}printf("删除失败,未找到键: %d\n", key);return0;//未找到键,删除失败}// 打印哈希表内容voidprintHashTable(HashTable* hashTable){if(hashTable ==NULL){printf("哈希表为空\n");return;}printf("\n=== 哈希表状态 ===\n");printf("大小: %d, 元素个数: %d\n", hashTable->size, hashTable->count);for(int i =0; i < hashTable->size; i++){if(hashTable->table[i]!=NULL){printf("桶[%d]: ", i); HashNode* curNode = hashTable->table[i];while(curNode !=NULL){printf("(%d:%d)", curNode->key, curNode->value);if(curNode->next !=NULL){printf(" -> ");} curNode = curNode->next;}printf("\n");}}printf("=================\n");}//释放哈希表voidfreeHash(HashTable* hashTable){if(hashTable ==NULL){return;}for(int i =0; i < hashTable->size; i++){ HashNode* curNode = hashTable->table[i];while(curNode !=NULL){ HashNode* temp = curNode; curNode = curNode->next;free(temp);}}free(hashTable->table);free(hashTable);printf("哈希表已释放\n");}//测试函数intmain(){printf("===== 哈希表测试程序 =====\n");//创建哈希表 HashTable* ht =createHashTable(5);if(ht ==NULL){return1;}//测试插入printf("\n1. 测试插入操作:\n");insertHash(ht,1,100);insertHash(ht,6,200);// 冲突:1和6在同一桶(1%5=1, 6%5=1)insertHash(ht,11,300);// 继续冲突(11%5=1)insertHash(ht,2,400);insertHash(ht,3,500);//打印当前状态printHashTable(ht);//测试更新printf("\n2. 测试更新操作:\n");insertHash(ht,1,150);// 更新键1的值printHashTable(ht);//测试查找printf("\n3. 测试查找操作:\n");int value;if(searchHash(ht,6,&value)){printf("找到键 6, 值为: %d\n", value);}else{printf("未找到键 6\n");}if(searchHash(ht,99,&value)){printf("找到键 99, 值为: %d\n", value);}else{printf("未找到键 99\n");}//测试删除printf("\n4. 测试删除操作:\n");DeleteHash(ht,6);DeleteHash(ht,99);// 删除不存在的键printHashTable(ht);//测试负键值printf("\n5. 测试负键值:\n");insertHash(ht,-4,600);// abs(-4)%5 = 4insertHash(ht,-9,700);// abs(-9)%5 = 4(冲突)printHashTable(ht);//清理freeHash(ht);return0;}结果:
===== 哈希表测试程序 ===== 创建哈希表成功,大小为:5 1. 测试插入操作: 插入成功: 键=1, 值=100 插入成功: 键=6, 值=200 插入成功: 键=11, 值=300 插入成功: 键=2, 值=400 插入成功: 键=3, 值=500 === 哈希表状态 === 大小: 5, 元素个数: 5 桶[1]: (11:300) -> (6:200) -> (1:100) 桶[2]: (2:400) 桶[3]: (3:500) ================= 2. 测试更新操作: 更新键:1 === 哈希表状态 === 大小: 5, 元素个数: 5 桶[1]: (11:300) -> (6:200) -> (1:150) 桶[2]: (2:400) 桶[3]: (3:500) ================= 3. 测试查找操作: 找到键 6, 值为: 200 未找到键 99 4. 测试删除操作: 删除键 6 成功 删除失败,未找到键: 99 === 哈希表状态 === 大小: 5, 元素个数: 4 桶[1]: (11:300) -> (1:150) 桶[2]: (2:400) 桶[3]: (3:500) ================= 5. 测试负键值: 插入成功: 键=-4, 值=600 插入成功: 键=-9, 值=700 === 哈希表状态 === 大小: 5, 元素个数: 6 桶[1]: (11:300) -> (1:150) 桶[2]: (2:400) 桶[3]: (3:500) 桶[4]: (-9:700) -> (-4:600) ================= 哈希表已释放