当前位置: 首页 > news >正文

哈希表原理与实现全解析

哈希表(散列表)

        哈希表(散列表):是一种高效的数据结构,通过“键-值”对存储数据,能在平均情况下实现快速的插入、删除和查找操作(时间复杂度接近 O(1))。

一、核心原理

        1.哈希函数:将输入的“键”(Key)转换为一个整数(哈希值),这个整数对应哈希表中的存储位置(索引)。

        例如:用“键”的 ASCII 码之和除以表长取余数,得到索引。

        2.存储结构:哈希表本质是一个数组,哈希值对应数组的索引,“值”(Value)就存在该索引位置。

        3.哈希存储:将要存储的数据的关键字和存储位置之间,建立起对应的关系,这个关系称之为哈希函数。存储数据时,通过对应的哈希函数可以将数据映射到指定的存储位置;查找时,仍可通过该函数找到数据的存储位置。

        4.哈希冲突/哈希矛盾:不同的“键”可能通过哈希函数得到相同的哈希值(冲突)。

二、解决哈希冲突/哈希矛盾的常用方法

链地址法(Chaining)

        1.核心原理:将哈希值相同的冲突元素,通过链表(或其他动态结构,如红黑树)存储在同一个“桶”(哈希表数组的索引位置)中。

        2.存储逻辑 :(1)当插入元素时,先用哈希函数计算键的哈希值,确定对应的桶索引;(2)若该桶为空,直接将元素作为链表头节点存入;(3)若该桶已有元素(发生冲突),则将新元素插入到该桶对应的链表末尾(或头部)。

        3.查找/删除逻辑:先通过哈希值定位到对应桶,再遍历桶中的链表,根据键匹配目标元素。

2. 开放地址法(Open Addressing)

        核心原理:当插入元素发生冲突时,按某种规则(探测序列)在哈希表中寻找下一个空闲位置存储,所有元素都直接存在数组中,不依赖额外数据结构。

两种方法对比:

链地址法开放地址法
存储结构数组+链表(或树)仅数组
空间开销较高(需要存储指针)较低
冲突处理将冲突元素,通过链表(或其他动态结构,如红黑树)存储在同一个“桶”(哈希表数组的索引位置)中。

按探测序列在哈希表中寻找下一个空闲位置存储

常见探测方法:

线性探测,二次探测,双重哈希

删除操作简单:只需从链表中移除节点,无需调整其他元素位置。复杂(需要标记“已删除”)
优点

 处理冲突灵活:冲突元素仅在同一桶的链表中存储,不会影响其他桶,也无需移动已有元素。

        空间利用率高:哈希表容量固定时,链表可动态扩展,适合元素数量不确定的场景(无需预先分配大量空间)。

        删除操作简单:只需从链表中移除节点,无需调整其他元素位置。

 空间效率高:无需存储链表指针,元素直接存在数组中,内存利用率高。

        局部性好:元素存储在连续或邻近的数组位置,缓存命中率高(适合内存受限场景)。

缺点

  额外空间开销:需要存储链表节点的指针/引用,空间利用率略低于开放地址法。

        极端情况效率退化:若哈希函数不佳,某一链表过长(如所有元素哈希到同一桶),查找效率会从 O(1) 退化到 O(n)(可通过链表转红黑树等优化,如 Java HashMap)。

 删除复杂:不能直接删除元素(否则会断裂探测序列),需标记为“已删除”,后续插入可复用该位置,但会影响查找效率。

        聚集效应:线性探测易导致冲突元素集中在某一区域(如插入多个元素后,连续索引被占用),使探测序列变长,效率退化。

        容量限制:哈希表装满后无法插入新元素(需提前扩容,且扩容成本高)

下面使用链地址法实现:1. 创建哈希表 2. 设计哈希函数 3. 插入数据 4. 查找数据 5. 销毁哈希表
6. 遍历

hash.h

#ifndef __HASH_H__
#define __HASH_H__#define HASH_TABLE_MAX_SIZE 27typedef struct per
{char name[32];char tel[32];
}Data_type_t;typedef struct node
{Data_type_t data;struct nod *pnext;
}HSNode_t;

hash.c

#include "hash.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>/*** @brief 哈希函数,根据字符计算哈希地址* @param key 用于计算哈希地址的字符(通常是姓名的首字母)* @return 计算得到的哈希地址(索引值)* @note 字母字符返回对应的0-25索引(a/A对应0,b/B对应1...z/Z对应25),*       非字母字符返回哈希表最大容量减1的索引*/
int hash_function(char key)
{// 处理小写字母if (key >= 'a' && key <= 'z'){return key - 'a';}// 处理大写字母else if (key >= 'A' && key <= 'Z'){return key - 'A';}// 处理非字母字符else{return HASH_TABLE_MAX_SIZE - 1;}
}/*** @brief 向哈希表中插入数据* @param hash_table 哈希表(二级指针,指向指针数组)* @param data 要插入的数据(包含姓名和电话信息)* @return 0表示插入成功,-1表示内存分配失败* @note 插入采用链表头插法的变种,会保持链表内数据按姓名升序排列*/
int insert_hash_table(HSNode_t **hash_table, Data_type_t data)
{// 计算插入位置(哈希地址)int addr = hash_function(data.name[0]);// 申请新节点保存数据HSNode_t *pnode = malloc(sizeof(HSNode_t));if (NULL == pnode){printf("malloc error\n");return -1;}// 初始化新节点数据pnode->data = data;pnode->pnext = NULL;// 若当前哈希地址对应的链表为空,直接插入if (NULL == hash_table[addr]){hash_table[addr] = pnode;}// 若链表非空,按姓名升序插入到合适位置else{// 新节点姓名小于等于头节点姓名,插入到链表头部if (strcmp(pnode->data.name, hash_table[addr]->data.name) <= 0){pnode->pnext = hash_table[addr];hash_table[addr] = pnode;}// 否则找到合适位置插入到链表中间或尾部else{HSNode_t *p = hash_table[addr];// 遍历链表,找到第一个比新节点姓名大的节点的前一个位置while (p->pnext != NULL && strcmp(p->pnext->data.name, pnode->data.name) < 0){p = p->pnext;}// 插入新节点pnode->pnext = p->pnext;p->pnext = pnode;}}return 0;
}/*** @brief 遍历哈希表并打印所有数据* @param hash_table 哈希表(二级指针,指向指针数组)* @note 按哈希地址顺序遍历,每个地址对应的链表数据会依次打印,地址间用空行分隔*/
void hash_for_each(HSNode_t **hash_table)
{// 遍历哈希表所有地址for (int i = 0; i < HASH_TABLE_MAX_SIZE; ++i){HSNode_t *ptmp = hash_table[i];// 遍历当前地址对应的链表while (ptmp){printf("%s : %s\n", ptmp->data.name, ptmp->data.tel);ptmp = ptmp->pnext;}// 每个地址的链表打印完后换行printf("\n");}
}/*** @brief 在哈希表中查找指定姓名的数据* @param hash_table 哈希表(二级指针,指向指针数组)* @param name 要查找的姓名* @return 找到的节点指针,未找到返回NULL* @note 先通过首字母计算哈希地址,再在对应链表中查找*/
HSNode_t *find_hash_table(HSNode_t **hash_table, char *name)
{// 计算查找的哈希地址(根据姓名首字母)int addr = hash_function(name[0]);// 在对应链表中遍历查找HSNode_t *ptmp = hash_table[addr];while (ptmp){// 比较姓名,相等则找到if (0 == strncmp(ptmp->data.name, name, strlen(name))){return ptmp;}ptmp = ptmp->pnext;}// 未找到return NULL;
}/*** @brief 销毁哈希表,释放所有动态分配的内存* @param hash_table 哈希表(二级指针,指向指针数组)* @note 逐个释放每个哈希地址对应的链表中的所有节点*/
void destroy_hash_table(HSNode_t **hash_table)
{// 遍历所有哈希地址for (int i = 0; i < HASH_TABLE_MAX_SIZE; i++){HSNode_t *pdel = hash_table[i];// 释放当前地址链表中的所有节点while (hash_table[i] != NULL){hash_table[i] = pdel->pnext; // 移动头指针到下一个节点free(pdel);                  // 释放当前节点pdel = hash_table[i];        // 更新待释放节点指针}}
}

main.c

#include<stdio.h>
#include"hash.h"int main(void)
{Data_type_t pers[5] = {{"zhangsan","110110"},{"lisi","120120"},{"wanger","119119"},{"zhaowu","122122"},{"maliu","10086"}};HSNode_t *hash_table[HASH_TABLE_MAX_SIZE] = {NULL};insert_hash_table(hash_table, pers[0]);insert_hash_table(hash_table, pers[1]);insert_hash_table(hash_table, pers[2]);insert_hash_table(hash_table, pers[3]);insert_hash_table(hash_table, pers[4]);}
http://www.dtcms.com/a/320233.html

相关文章:

  • 天道20金句
  • Moses工具的配置和小语种平行语料训练SMT完整实现
  • 大模型 Transformer模型(上)
  • Java集合的遍历方式(全解析)
  • 力扣经典算法篇-46-阶乘后的零(正向步长遍历,逆向步长遍历)
  • BGP笔记整理
  • Maven高级:继承与聚合实战指南
  • RS485转Profibus网关在QDNA钠离子分析仪与300PLC通信中的应用解析
  • 【OCCT+ImGUI系列】013-碰撞检测-包围盒Bnd_Box
  • 【入门级-C++程序设计:9、函数与递归-函数定义与调用、形参与实参】
  • RESTful 服务概述:从理念到实践的全面解析
  • Coze开放平台综合文档指南
  • 达梦包含OR条件的SQL特定优化----INJECT-HINT优化方法
  • 最新完整内、外期货量化交易系统C#源码可售
  • 【C#补全计划:类和对象(九)】接口
  • redis--黑马点评--用户签到模块详解
  • dubbo源码之编解码逻辑
  • 一场 Dark Theme A/B 测试的复盘与提效实践
  • 聚集索引VS非聚集索引:核心差异详解
  • rebase 和pull的通俗区别是什么
  • 一个基于固定 IP地址查询天气的 C 语言程序,通过调用第三方天气 API:
  • React 多语言(i18n)方案全面指南
  • 计算机英语详细总结
  • 本地化密码恢复工具的技术实现与应用边界
  • RabbitMQ面试精讲 Day 13:HAProxy与负载均衡配置
  • Git `cherry-pick` 工具汇总
  • Docker 加载镜像时出现 “no space left on device” 错误的解决方法
  • Java Lambda表达式:简洁高效的函数式编程
  • 关于光猫研究
  • 【代码随想录day 14】 力扣 101. 对称二叉树