一、哈希表
哈希表h[ ]的存储结构是为了实现O(1)的复杂度查找一个数。
通过哈希函数f(x)完成映射,由关键字key得到哈希表中的下标idx:idx=f(key);
然后进行存储:h[idx]=key;
-
构建哈希函数: 除留取余法
f(x) = x mod p 将x映射到 0到p-1 之间的数。
其中:p要取成一个质数,而且要离2的整次幂尽可能远。
-
解决冲突问题:当f(key1)==f(key2)时
- 拉链法,一个h[i]处连一个单链表
- 开放寻址法,冲突则往后顺移,因此 h 的长度一般定义为规定范围的2到3倍
题目描述
维护一个集合,支持如下几种操作:
“I x”,插入一个数x;
“Q x”,询问数x是否在集合中出现过;
现在要进行N次操作,对于每个询问操作输出对应的结果。
输入格式
第一行包含整数N,表示操作数量。
接下来N行,每行包含一个操作指令,操作指令为”I x”,”Q x”中的一种。
输出格式
对于每个询问指令“Q x”,输出一个询问结果,如果x在集合中出现过,则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1≤N≤105
−109≤x≤109
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No
1. 链地址法
和开放寻址法相比,哈希表的长度和实际要求的长度N一样即可。但是每个位置要引出一个单链表,共N条单链表,将所有f(key)为同一地址的映射在同一条单链表中。
因此,哈希表h中每个位置存储的不再是key值,而是作为一个头结点指向单链表,单链表为不包含头结点的单链表。
N个单链表的总长度也为N,理想情况下,每个单链表只有一个结点(不发生冲突)。单链表用之前学过的数组模拟,用一个数组模拟多个链表(因为有多个头结点指向)。
#include <iostream>
#include <cstring>
using namespace std;
const int N=1e5+3; "取大于1e5的第一个质数,要模质数"
int h[N]; //哈希表,作为head结点指向存储数值的e链表 ,冲突的存在同一位置
int e[N],ne[N],idx;
//这里可以从0开始存储,每个头结点都是单独拉出的head指向的
//ne[i]=-1作为链表结束的标志,表示后面没有链表了,为了避免单独考虑初始化的问题,h数组初始化为-1,初始表示空,指向结尾
//memset数组适合初始化0(00000000) -1(11111111) 0x3f3f3f3f3f 是按字节初始化
void insert(int x)
{
"x属于-10^9到 10^9 ,模N后属于-(N-1)到N-1,要将其映射到正数的区间(0,N-1)"
int k=(x%N+N)%N; "-(N-i)和i模N同余"
e[idx]=x;
ne[idx]=h[k]; //每次采用头插法
h[k]=idx++;
}
bool find(int x)
{
int k=(x%N+N)%N;
for (int i = h[k]; i != -1; i = ne[i]) {
if (e[i]==x) return true;
}
return false;
}
int main()
{
int n,x;
char op[2];
cin>>n;
memset(h, -1, sizeof(h)); //头文件 cstring
while (n--){
cin>>op>>x;
if (op[0]=='I'){
insert(x);
} else {
if (find(x)) puts("Yes");
else printf("No\n");
}
}
return 0;
}
2. 开放寻址法
欲插入数值x,通过f(x)找到第一次要插入的位置,
如果位置为null,则代表数组中不存在该数,这个下标是要插入的位置;
如果不为空且该值不为x,采用线性探测序列,依次往后顺移一位,继续判断。
循环结束有两种可能:1. 碰到了null,代表了要插入的位置;2. 不为空,说明该值为x,已经查到该数。
注意:此法中哈希表定义的长度要是实际要求长度的2到3倍。
#include <iostream>
#include <cstring>
using namespace std;
const int N=2e5+3,null=0x3f3f3f3f;
int h[N];
//这次h数组里不存指向的链表了,而是就是存真实的数值,
//由于原数组值是-10^9到10^9,所以要取一个不是这个范围的数初始化h数组
int find(int x)
{
int k=(x%N+N)%N;
while (h[k]!=null && h[k]!=x){
k++;
if (k==N) k=0; //循环
}
//退出循环时,要么为空,要么h[k]=x,因此返回的k要么是插入的位置,要么是匹配的位置
return k;
}
int main()
{
int n,x;
char op[2];
cin>>n;
memset(h, 0x3f, sizeof(h)); //头文件 cstring
while (n--){
cin>>op>>x;
int k=find(x);
if (op[0]=='I'){
h[k]=x;
} else {
if (h[k]!=null) puts("Yes");
else printf("No\n");
}
}
return 0;
}
二、字符串哈希
详细介绍见蓝字部分。
用途:和KMP作用一样,用于字符串匹配。
思路:给定一个父串(从下标1开始存储),长度为N,将字符串视为一个P进制数,每个字符的数值可以取其ASCII码,也可以自己编码(一定要从1开始编码),然后按照前缀和的思想,将其转化为N个子串,依次求这N个子串的在P进制数下对应的数值,然后模Q,存在数组的下标1到N中。
求父串中从l到r的字符串对应的数值:h[r]-h[l-1]×pr-l+1
通常P取131 或者13331 ;Q取264。
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
const int N=1e5+10,P=131;
char str[N];
int l1,r1,l2,r2;
ULL h[N],p[N];//p[N]={1} 也可,除了第一位是1,其它都是0
//h[k]存储字符串前k个字母的哈希值 mod 2^64;h[0]=0,从下标1开始,前缀和思想
// p[k]存储 P^i,方便求子串,任何数的次方都为1,因此p[0]=1
ULL get_sub(int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
int n,m;//长度为n的字符串,再给定m个询问
cin>>n>>m>>str+1;
p[0]=1;
//前缀和思想求P进制数
for (int i = 1; i <= n; ++i) {
p[i]=p[i-1]*P;//顺便求P^i
h[i]=h[i-1]*P+str[i]; //由于字符串里有大写 小写 数字,直接就按照ASCII码运算,不再规定A为1了,当全是大写字母时,可以str[i]-'A'+1,即A从1开始编码,而不用ASCII码
} //前缀求哈希值,每次做左移一位
while (m--){
cin>>l1>>r1>>l2>>r2;
if (get_sub(l1,r1)==get_sub(l2,r2)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
return 0;
}
利用字符串哈希可以实现O(n)复杂度实现字符串匹配,而KMP算法需要O(n2)复杂度,但是字符串哈希有出错概率,虽然极小。
KMP模板题传送
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
const int N=1e5+10,M=1e6+10,R=131; 模板串p 模式串s R进制
char s[M],p[N]; p短 s长,p匹配s
ULL h[M],r[M]={1}; 即r[0]=1,其它值为0
ULL P; 大写p存模板串的哈希值
int main() {
int n,m;
cin>>n>>p+1>>m>>s+1;
求模板串p的哈希值
for (int i = 1; i <= n; ++i) P=P*R+p[i];
求模式串s的前缀和
for (int i = 1; i <= m; ++i) {
r[i]=r[i-1]*R;
h[i]=h[i-1]*R+s[i];
}
for (int i = 1; i <= m-n+1; ++i) {
if (h[i+n-1]-h[i-1]*r[n]==P) cout<<i-1<<" ";
}
return 0;
}