Acwing 840. 模拟散列表
题面:
维护一个集合,支持如下几种操作:
I x
,插入一个整数; Q x
,询问整数是否在集合中出现过 现在要进行
次操作,对于每个询问操作输出对应的结果。
哈希表[1]
基本概念
哈希表也叫散列表,通过将键映射到索引位置(在关键字和位置之间建立对应关系)来实现高效的查找操作。
记作
时间复杂度:
无论哈希表中有多少条数据,其插入和查找操作的时间复杂度都恒定。
因为哈希表的查找速度非常快,所以在很多程序中都有使用哈希表,例如拼音检查器。
基本思想:转换思想,通过哈希函数将非int的键或者关键字转换成int数组下标。
注意:并不是每个元素都需要通过哈希函数来将其转换成数组下标,有些可以直接作为数组的下标。
缺点:基于数组,空间效率较低,扩容成本较高;
当哈希表被填满时,会造成比较严重的性能下降。
索引存储和散列存储的区别:散列存储多了
举例:用哈希表来存放班级里面学生信息
利用学号作为关键字时,因为数据类型为int,可以同时作为数据下标,不需要通过哈希函数进行转化;
但如果需要将姓名作为关键字,就需要哈希函数完成键值的重映射。
常见的哈希函数[2]
构造准则:简单、均匀、冲突最小
构造哈希函数的目标是使得到的
1. 线性定址法
直接取关键字的某个线性函数作为存储地址:
其中
优点:直接定址所得地址集合和关键字集合的大小相同;对于不同的关键字不会产生冲突。
缺点:空间效率低,占用连续地址空间,需要提前确定关键字的取值范围,且不能太大。
举例:有一个从1岁到100岁的人口统计表,其中,年龄作为关键字,哈希函数取其自身,即哈希函数为
2. 除留余数法
将关键字对某一小于散列表长度的数
最简单,也最常用。不仅可以对关键字直接取模,也可在对关键字进行折迭、平方取中等运算之后取模。
对
3. 数字分析法
取某关键字的某几位组合成哈希地址。
假设已经知道哈希表中所有的关键字值,而且关键字值都是数字;
所选的位应当是:各种符号在该位上出现的频率大致相同。
举例:有1000个记录,关键字为10位十进制整数
假设经过分析,各关键字中
例如,
4. 平方取中法
对关键字取平方,然后将得到结果的中间几位作为存储地址;
当无法确定关键字中哪几位分布较均匀时,先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。
特点:通过平方扩大差别,另外,中间几位与关键字中的每一位都相关,故不同关键字会以较高的概率产生不同的、均匀的哈希地址。
5. 折叠法
将关键字自左到右分成位数相等的几部分,最后一部分可以短些;
然后将这几部分叠加求和,并按哈希表表长,取后几位作为哈希地址。
适用于:每一位上各符号出现概率大致相同的情况。
举例:根据国际标准图书编号(ISBN)建立一个哈希表。
如一个国际标准图书编号
- 移位法:将各部分的最后一位对齐相加;
使用移位叠加 ,故 (将分割后的每一部分的最低位对齐)。 - 间界叠加法:从一端向另一端沿分割界来回折叠后,最后一位对齐相加。
使用边界叠加法叠加 ,故 (从一端向另一端沿分割界来回叠加)。
6. 随机数法
取关键字的随机函数值为它的哈希地址,即
其中
适用于:关键字长度不等
哈希冲突与处理方法
哈希冲突:元素关键字不同,但具有相同哈希地址,键值不再一一对应。
一般来说,哈希冲突很难避免。为了解决这个问题,需要找到新的尚未被占用的地址来存储该元素。
拉链法
将所有的冲突元素用单链表连接起来,每个链表对应一个单元;
此时,每个单元存储的不再是元素本身,而是相应单链表的头指针。
思路类似于:邻接表
基于取模法的哈希函数:(x % N + N) % N
通常情况下,我们希望哈希函数的输出值处于
而转换前的键值范围例如
为了解决这个问题,可以利用取模运算的性质:如果
单链表的建立:AcWing 826. 单链表 - 蒟蒻爬行中
此处插入操作选用头插法,链表头指针即存储在哈希表中。
#include<bits/stdc++.h> using namespace std; const int N = 100003; //从100000开始的第一个素数 int n, x; int h[N], e[N], ne[N], idx; /* 构建哈希函数:取模法 */ int hashF(int x) { return (x % N + N) % N; //索引值 } /* 拉链法的插入操作 */ void insert(int x) { int k = hashF(x); e[idx] = x; //数据数组 ne[idx] = h[k]; //指针数组 h[k] = idx++; //哈希数组 } /* 拉链法的查找操作 */ bool find(int x) { int k = hashF(x); //先找到索引值,再从该单元链表头开始查找 for (int i = h[k]; i != -1; i = ne[i]) if (e[i] == x) return true; return false; } int main() { cin >> n; memset(h, -1, sizeof h); //空指针一般用-1表示 while (n--) { char op; cin >> op >> x; if (op == 'I') insert(x); else cout << (find(x) ? "Yes\n" : "No\n"); } }
开放寻址法
当发生哈希冲突时,则开始寻找,直到得到一个新空闲地址,再插入该元素。
寻找新地址的过程也成为再哈希,一般采用线性探测法或者平方探测法。
线性探测法:从发生冲突的地址开始,依次探测下一个地址,直到找到一个空闲单元为止。
和拉链法的区别:
- 只利用一个一维数组,形式更为简单;
- 数组的长度更长,一般为输入数据量的2~3倍。
#include<bits/stdc++.h> using namespace std; const int N = 200003; //从200000开始的第一个素数 const int null = 0x3f3f3f3f; //大于10e9的数 int n, x, h[N]; /* 构建哈希函数:取模法 */ int hashF(int x) { return (x % N + N) % N; //索引值 } /* 开放寻址法的相关操作 */ //查找:若可以寻找到k,k就是x的位置 //插入:若不能寻找到k,k就是x应该存储的位置 int find(int x) { int k = hashF(x); //若单元中已有元素且不为x,开始寻址 while (h[k] != null && h[k] != x) { k++; //若已经遍历完所有单元,则回到表首地址 if (k == N) k = 0; } //否则直接返回k单元 return k; } int main() { cin >> n; memset(h, 0x3f, sizeof h); while (n--) { char op; cin >> op >> x; int k = find(x); if (op == 'I') h[k] = x; else cout << (h[k] != null ? "Yes\n" : "No\n"); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!