基本思想:
直接通过关键字 key,算出存储位置,跳过逐个比较查找,实现近似常数时间的增删查。
普通数组:下标是已知数字,按下标访问 O (1); 普通链表 / 顺序表:找元素要从头挨个比对,
O (n)。
哈希表的思路:把任意类型的关键字 key,经过哈希函数换算,变成数组下标,直接跳到这个下标位置存取数据,不用遍历。
哈希冲突
关键字数量 > 数组长度,必然出现:两个不同 key,哈希算出同一个下标,就是哈希冲突。
比如表长 10,13%10=3,23%10=3,两个 key 映射到同一个位置。 哈希函数只能减少冲突,不能完全消除冲突,所以必须有冲突处理方案。
冲突处理两大原理
- 拉链法:数组每个位置挂一条链表。映射到同一个下标的全部节点,都存到这条链表里。访问时先找下标,再遍历链表比对 key。
- 开放寻址法:全部数据放在哈希数组内部,冲突就按照规则向后寻找数组里的空位置存放。
本次学习使用拉链法,进行数据存储,降低哈希冲突。
在对于哈希函数构造方面,有多种方法,具体简介如下,本次学习代码主要使用除留取余法:
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;
}
i=0,外层循环逐个访问哈希表数组下标;- 如果
hash_table[i]==NULL,这个槽没有链表,直接 i++ 跳过; - 槽位不为空,
p = hash_table[i],p 指向链表第一个有效节点; - 内层循环:
p != NULL时打印p->data,p=p->next向后走; - 内层循环结束代表当前链表全部打印完毕;
- i++ 继续下一个槽,外层循环走完所有槽;
- 打印换行,return 1。
哈希表单个参数hash_delete_key 删除函数
-
根据待删除 data 计算哈希槽位
addr = data%SIZE。 -
判断该槽位
hash_table[addr]为空,链表不存在,直接返回 - 1 删除失败。 -
槽位不为空,p 指向该槽链表头节点。
-
判断
p->next == NULL,链表仅有一个节点:如果节点数据等于待删 data,释放节点,槽位置 NULL;不匹配直接往下走。 -
链表节点数大于 1:先判断头节点就是要删除的节点,修改哈希表头指针指向头的下一个节点,释放原头节点。
-
头节点不是目标,进入 while 循环,p 往后移动,直到
p->next为空或者p->next->data等于待删数据。 -
循环结束判断,如果
p->next == NULL说明整条链表没找到目标,返回‑1。 -
找到待删节点,用 p_temp 保存待删节点地址,修改 p 的 next 跳过该节点,free 释放 p_temp。
-
函数最后统一返回 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;
}
哈希表销毁:进删除哈希表内的链表的全部数据,并不销毁哈希表本身
-
i 初始化为 0,外层 while 循环遍历哈希表全部 10 个槽位。
-
判断
hash_table[i]不为 NULL,说明该槽存有链表,取头指针给遍历指针 p。 -
内层 while 循环,p 不为空时,用 temp 暂存 p 的下一个节点地址。
-
free 释放 p 指向的节点,再把 p 赋值为 temp,处理下一个节点。
-
内层循环结束,本条链表全部释放,将
hash_table[i]置为 NULL。 -
i 自增,处理下一个哈希槽。
-
全部槽遍历完成,返回 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;
}