数据结构-哈希表

基本思想:

直接通过关键字 key,算出存储位置,跳过逐个比较查找,实现近似常数时间的增删查。

普通数组:下标是已知数字,按下标访问 O (1); 普通链表 / 顺序表:找元素要从头挨个比对,

O (n)。

哈希表的思路:把任意类型的关键字 key,经过哈希函数换算,变成数组下标,直接跳到这个下标位置存取数据,不用遍历。

哈希冲突

关键字数量 > 数组长度,必然出现:两个不同 key,哈希算出同一个下标,就是哈希冲突。

比如表长 10,13%10=3,23%10=3,两个 key 映射到同一个位置。 哈希函数只能减少冲突,不能完全消除冲突,所以必须有冲突处理方案。

冲突处理两大原理

  1. 拉链法:数组每个位置挂一条链表。映射到同一个下标的全部节点,都存到这条链表里。访问时先找下标,再遍历链表比对 key。
  2. 开放寻址法:全部数据放在哈希数组内部,冲突就按照规则向后寻找数组里的空位置存放。

本次学习使用拉链法,进行数据存储,降低哈希冲突。

在对于哈希函数构造方面,有多种方法,具体简介如下,本次学习代码主要使用除留取余法:

1. 直接定址法

公式: hash(key)=a key + b) 直接用关键字本身做线性变换得到下标。

  • 优点:没有冲突,计算极快
  • 缺点:只适合 key 连续、值域范围小;key 分散时数组会巨大,浪费空间。
  • 例子:学生学号 202601~202650,直接映射。

2. 除留余数法(最常用)

hash(key)=a×key+b

p 一般取小于哈希表长的质数

  • 要点:p 选质数,能让 key 分布更均匀,减少聚集冲突。
  • 优点:简单,整数 key 首选;C 语言示例 key % tableSize
  • 缺点:选的 p 不好会大量冲突。

3. 数字分析法

针对数字型关键字,提取数字中分布比较随机的几位作为哈希地址。

例:一批手机号,前面几位大多相同,取末尾 4 位作为下标。

  • 使用场景:事先知道 key 的全部样本。

4. 平方取中法

把 key 做平方,取平方结果中间的若干位作为哈希地址。 平方后中间位受 key 每一位都影响,随机性变好。

示例:key=1234,平方 = 1522756,取中间 3 位 227 作为下标。

  • 适合关键字各位分布不太均匀的情况。

5. 折叠法

把 key 分割成几部分,叠加求和,再取部分作为哈希下标。 分移位折叠、边界折叠。 例:长的身份证号,切成几段相加。

适合关键字位数很多、数值很大。

6. 随机数法

hash(key)=random(key) 用随机函数把 key 映射,适合 key 长度变化很大的浮点数。

创建哈希表:使用链表降低哈希冲突概率,在哈希表中仅存储链表的首节点地址数据

复制代码
typedef int data_t;

typedef struct node
{
	data_t data;
	struct node *next;
}node_t;

node_t *hash_table[10];
data_t a[] = {21,33,43,54,63,67,82,34};


int  hash_creat_two(data_t *a ,int len)
{
	if(a == NULL)
	{
		printf("a is NULL\n");
		return -1;
	}
    //遍历传入的数组数据
	int i = 0;
	while(i<len)
	{
        //取余数法在对于的下标的哈希表位置创建链表节点
		int addr = a[i]%10;
        //如果当前位置还是空的,还没有节点在里面,就直接创建节点,然后哈希表存储第一节点数据
		if(hash_table[addr] == NULL)
		{
			node_t *new = malloc(sizeof(node_t));
			if(new == NULL)
			{
				printf("new node malloc fail\n");
				return -1;
			}

			new->data = a[i];
			new->next = NULL;
			hash_table[addr] = new;
		}else
		{   //当前位置已经有节点了,不是空的,需要使用头插或者尾插,插入新的节点,这里使用头插
			node_t *new = malloc(sizeof(node_t));
			if(new == NULL)
			{
				printf("new node malloc fail\n");
				return -1;
			}
			new->data = a[i];
			new->next = hash_table[addr];
			hash_table[addr] = new;
		}
		i++;
	}
	return 1;
}

哈希表hash_table[10]是指针数组,数组每个元素存放一条链表的头指针;哈希函数:addr = a[i] % 10,把数据对 10 取余得到哈希下标,冲突的时候用链表头插法挂载节点。

函数 hash_creat_two 接收待插入数组 a 与数组长度 len,首先校验传入数组指针是否为空,若为空则打印提示并返回 - 1 表示执行失败,随后初始化下标 i 为 0 进入 while 循环遍历数组,循环条件为 i 小于数组长度,每一轮循环中取出数组当前元素,通过 a i 对 10 取余数计算得到哈希表的槽位下标 addr,接着判断 hash_table addr 是否为 NULL,若该槽位为空就调用 malloc 申请链表节点内存,内存分配失败则打印提示返回 - 1,给新节点存入数据,将节点 next 置为空,再让哈希表对应下标指向这个新节点作为链表首节点;若槽位不为空代表发生哈希冲突,同样申请新节点内存并存入数据,执行头插操作,先把新节点的 next 指向该槽位原来的链表头,再更新哈希表槽位指针指向新节点,使新节点成为新的链表头部,完成单次插入后 i 自增继续循环处理下一个数组元素,当 i 大于等于数组长度时循环结束,所有数据插入完毕后函数返回 1 代表构建成功,

哈希表数据全打印:函数用于遍历打印链地址法哈希表中的全部数据

复制代码
int hash_print(int len)
{
	int i = 0;
    //遍历哈希表的每一个元素
	while(i<len)
	{
        //如果该位置的地址值非0则代表存在有效数据
		if(hash_table[i] != NULL)
		{
			node_t *p = hash_table[i];
            //链表从首节点遍历打印
			while(p!=NULL)
			{
				printf("%d ",p->data);
				p = p->next;
			}
		}
		i++;
	}
	putchar('\n');
	return 1;
}
  1. i=0,外层循环逐个访问哈希表数组下标;
  2. 如果hash_table[i]==NULL,这个槽没有链表,直接 i++ 跳过;
  3. 槽位不为空,p = hash_table[i],p 指向链表第一个有效节点;
  4. 内层循环:p != NULL时打印p->datap=p->next向后走;
  5. 内层循环结束代表当前链表全部打印完毕;
  6. i++ 继续下一个槽,外层循环走完所有槽;
  7. 打印换行,return 1。

哈希表单个参数hash_delete_key 删除函数

  1. 根据待删除 data 计算哈希槽位addr = data%SIZE

  2. 判断该槽位hash_table[addr]为空,链表不存在,直接返回 - 1 删除失败。

  3. 槽位不为空,p 指向该槽链表头节点。

  4. 判断p->next == NULL,链表仅有一个节点:如果节点数据等于待删 data,释放节点,槽位置 NULL;不匹配直接往下走。

  5. 链表节点数大于 1:先判断头节点就是要删除的节点,修改哈希表头指针指向头的下一个节点,释放原头节点。

  6. 头节点不是目标,进入 while 循环,p 往后移动,直到p->next为空或者p->next->data等于待删数据。

  7. 循环结束判断,如果p->next == NULL说明整条链表没找到目标,返回‑1。

  8. 找到待删节点,用 p_temp 保存待删节点地址,修改 p 的 next 跳过该节点,free 释放 p_temp。

  9. 函数最后统一返回 0 代表删除成功。

    // 根据数据值删除哈希表中对应的节点
    int hash_delete_key(data_t data)
    {
    //思路:
    //1.表中如果没有这个值,没法删,返回失败
    //2.存在该数据
    // 链表可能就只有一个节点;也可能有多个节点
    // 需要找到待删节点的前驱节点,完成断链删除

    复制代码
     //计算待删除数据对应的哈希槽位下标
     int addr = data%SIZE;
     //该槽位没有链表,数据一定不存在
     if (hash_table[addr] == NULL)
     {
     	return -1;  //删除失败
     }else 
     {
     	node_t *p = hash_table[addr];  //p指向当前槽链表头节点
     	
     	//情况1:链表只有唯一一个节点
     	if(p->next == NULL)
     	{
     		if (p->data == data)   //该唯一节点就是要删除的节点
     		{
     			free(p);                 //释放节点内存
     			hash_table[addr] = NULL; //哈希槽置空
     		}
             //BUG:如果唯一节点数据不匹配,这里没有返回失败,会走到最后的return 0
     	}else 
     	{
     		//情况2:链表节点数量大于1
     		if (p->data == data)  //待删除的是链表头节点
     		{
     			hash_table[addr] = p->next; //哈希表头更新为下一个节点
     			free(p);                    //释放原来的头节点
     		}else 
     		{
     			//头节点不是目标,向后找待删节点的前驱
     			//循环:下一个节点不为空 并且 下一个节点数据不等于目标,就继续后移
     			while (p->next && p->next->data != data)
     			{
     				p=p->next;
     			}
     			//循环结束,p->next就是待删节点;如果p->next为NULL代表没找到
     			if (p->next == NULL)
     			{
     				return -1; //链表遍历完没有找到目标,删除失败
     			}
     			node_t *p_temp = p->next; //p_temp保存待删除节点地址
     			p->next = p_temp->next;   //断链,跳过待删除节点
     			free(p_temp);             //释放待删除节点内存
     		}
     	}
     }
     return 0; 

    }

哈希表数据查找更新:在哈希表中查找旧值 old,找到后将节点的数据更新为新值 new,返回被修改节点的指针,找不到则返回 NULL。

  • 根据要查找的旧值 old 计算哈希槽位 addr = old%10。

  • 判断 addr 越界,原代码此处条件存在 bug。

  • 取对应槽位链表头指针赋值给遍历指针 p。

  • while 循环,p 不为空就比对当前节点 data 是否等于 old。

  • 匹配成功,把节点数据修改为 new,返回当前节点指针。

  • 不匹配,p = p‑>next,向后移动遍历下一个节点。

  • 链表遍历完毕没有找到目标,打印提示,返回 NULL。

    node_t *hash_updata(data_t old,data_t new)
    {
    int addr = old%10; //根据旧数据计算哈希槽位
    if(addr>=10 || addr <0) //判断addr是否越界
    {
    printf("not found\n");
    return NULL;
    }else
    {
    node_t *p = hash_table[addr]; //取对应槽位链表头指针
    while(p!=NULL) //遍历该槽位整条链表
    {
    if(p->data == old) //找到存储old的节点
    {
    p->data = new; //修改数据为new
    return p; //返回被修改节点地址
    }
    p = p->next; //指针后移
    }
    }
    printf("not found\n"); //遍历完链表没有找到目标数据
    return NULL;
    }

哈希表销毁:进删除哈希表内的链表的全部数据,并不销毁哈希表本身

  1. i 初始化为 0,外层 while 循环遍历哈希表全部 10 个槽位。

  2. 判断hash_table[i]不为 NULL,说明该槽存有链表,取头指针给遍历指针 p。

  3. 内层 while 循环,p 不为空时,用 temp 暂存 p 的下一个节点地址。

  4. free 释放 p 指向的节点,再把 p 赋值为 temp,处理下一个节点。

  5. 内层循环结束,本条链表全部释放,将hash_table[i]置为 NULL。

  6. i 自增,处理下一个哈希槽。

  7. 全部槽遍历完成,返回 1,销毁结束。

    int hash_destroy(void)
    {
    int i = 0;
    while(i<10) //外层遍历哈希表每一个槽位
    {
    if(hash_table[i] != NULL) //当前槽存在链表
    {
    node_t *p = hash_table[i]; //p指向链表头
    while(p!=NULL)
    {
    node_t *temp = p->next; //先保存下一个节点,防止free后丢失后继地址
    free(p); //释放当前节点
    p = temp; //p移动到原先下一个节点
    }
    hash_table[i] = NULL; //槽位头指针置空,避免野指针
    }
    i++;
    }
    return 1;
    }

相关推荐
君君思密达4 小时前
Leetcode Hot 100 题目详解-哈希表
数据结构·算法·leetcode
lucas_AI4 小时前
Muse Glimmer 30B:Meta 难得给的真开源,强在哪、虚在哪
人工智能·算法
ai产品老杨4 小时前
国产NPU视觉算法完整流程
算法
过期的秋刀鱼!4 小时前
带替换的采样
人工智能·python·算法·决策树·机器学习
过期的秋刀鱼!5 小时前
使用多个决策树
人工智能·算法·决策树·机器学习·数据挖掘
203号居民5 小时前
LeetCode hot 100 —560. 和为 K 的子数组
java·算法·leetcode
Doraemomo5 小时前
数据结构-双向链表与多文件工程整合之makefile基础
数据结构·链表
退休倒计时6 小时前
【每日一题】LeetCode 20. 有效的括号 TypeScript
算法·leetcode·职场和发展·typescript
文心快码BaiduComate6 小时前
光本位联合文心快码打造面向光电芯片研发的全栈AI Agent Lightmate
算法