哈希
哈希表:将大而复杂的数据映射到紧凑的区间内。分为:①存储结构
(离散化是特殊的哈希,之前讲的离散化是严格保序的 映射到区间上是连续递增的)
哈希不保序,这里讲的是一般的哈希
弊端:若数据的数值较大可以对其取模后再映射,但是取模后可能造成:
之前不相同的数值取模后映射到同一位置上了,所以引入:开放寻址法,拉链法
①存储结构
{
1(开放寻址法
{
添加:
开一个一维数组h[],长度一般要开到题目要求长度的2到3倍
1'假设a取模后的值为k,那么就将a放到h[k]上
2'若b取模后的值仍为k,那么就将b放到h[k+i]上(即向后遍历,直到找到一个没存放数值的h[k+i]为止)
注意:由于这里数组h[ ]的长度为题设要求的2~3倍,所以一定能找到一个位置满足上述1’,2’两种情况
...
查找:
如果当前h[k]存在并且h[k]==x那么就找到了,如果!=x那么就继续向下寻找
如果当前h[k]不存在,那么就不需要向下寻找,即x不存在
删除:
不真正的删除,而是开一个bool类型的数组,将所要删除的数值标记一下
后续在查询、添加操作时,对此值是否存在特判一下即可
题目:840. 模拟散列表 - AcWing题库
代码:
暴力+二分,样例11/16:
#include<bits/stdc++.h>using namespace std;const int N=1e5+50;
int f[N];int main()
{int n,i=0;scanf("%d",&n);while(n--){char op[5];int x;scanf("%s %d",op,&x);if(!strcmp(op,"I")){f[i]=x;i++;}else{sort(f,f+i);int l=0,r=i-1;while(l<r){int mid=l + r +1 >> 1;if(f[mid]>x) r=mid-1;else l=mid;}if(f[l]==x) cout << "Yes" << endl;else cout << "No" << endl;}}return 0;
}
哈希:
#include<iostream>
//memset()函数所在头文件
#include<cstring>
using namespace std;
int n;
const int N=2e5+3,null=0x3f3f3f3f;
int h[N];//如果x在hash表中已经存在的话,就返回x在hash表中所在的位置
//如果不存在的话就返回x应该在hash表中存储的位置
int find(int x)
{int k=(x%N + N)%N;while(h[k]!=null && h[k]!=x){k++;//如果k==N的话(最边缘),那么就让它从头开始寻找//由于N的长度为题解要求的2倍,所以一定能找到一个k//这个k要么是x在hash表中的位置,要么是它可以在hash表中存在的位置if(k==N) k=0;}return k;}int main()
{scanf("%d",&n);memset(h,null,sizeof h);while(n--){char op[2];int x;scanf("%s %d",op,&x);int k=find(x);if(!strcmp(op,"I")) h[k]=x;//直接记录下x可以在hash表中插入的位置else{//如果x存在于hash表中,那么一定不等于null//如果不存在于hash表中,那么k所返回的值就是x可以在hash表中存在的位置//但是由于这一步是查询操作,所以h[k]==null(null是初始值)if(h[k]!=null) cout << "Yes" << endl;else cout << "No" << endl;}}return 0;
}
}
2(拉链法
{
添加:
开一个一维数组a[]存下所有数值,将数值a取模后的值(假设是3),
就在数组a[3]的后面 “拉一条链” 将a链住
如果又有数值b取模后仍等于3,那么就在数值a的后面再“拉一条链”将数值b链住
(每个数组a[]下对应的链就是我们之前所学的单链表)
插入过程与前面链表的插入过程一致,可翻阅查看:.单链表.-CSDN博客
...
查找:
求出数值取余后对应的是数组a[]上的那一个值,再询问此a[]上的链表是否有所要求的数值
删除:
不真正的删除,而是开一个bool类型的数组,将所要删除的数值标记一下
后续在查询、添加操作时,对此值是否存在特判一下即可
代码:
#include<iostream>
//memset()函数所在头文件
#include<cstring>
using namespace std;
int n;
const int N=1e5+3;
int h[N],e[N],ne[N],idx;void insert(int x)
{//取模,映射到数组h[]上,x可能为负数,所以先模再加 之后再次取模int k=(x%N+N)%N;e[idx]=x,ne[idx]=h[k],h[k]=idx,idx++;
}bool find(int x)
{int k=(x%N + N)%N;//遍历链表h[k](链表上的第一个下标)表示数组上下标为k的下一个指向//每次让i指向它的下一个指向(移动指针i,使其继续向下遍历)//i=-1时即遍历结束(空值)for(int i=h[k];i!=-1;i=ne[i])if(e[i]==x)return true;return false;
}int main()
{scanf("%d",&n);memset(h,-1,sizeof h);while(n--){char op[2];int x;scanf("%s %d",op,&x);if(!strcmp(op,"I")) insert(x);else{if(find(x)) cout << "Yes" << endl;else cout << "No" << endl;}}
return 0;
}
}
取模的值:
上述的N分别取的是N=2e5+3、1e5+3这里不像之前取1e5+10是因为:N是取模的值
取模的值一般满足以下规律使映射时的重复率最低:(取模的数值一般取成质数并且离2的整次幂要远的多)
计算方法:
(取模的数值一般取成质数并且离2的整次幂要远的多)
{//本题的操作次数不大于100000,所以从100000开始遍历(根据题设要求来初始化i)for(int i=100000;;i++){bool flag=true;for(int j=2;j*j<=i;j++)if(i%j==0){flag=false;break;}if(flag){cout << i << endl;break;}}return 0;//这里输出的i就是接下来要取模的数值
}
②字符串哈希方式
字符串hash方式
这里讲的是特殊的hash方式:字符串前缀哈希法
类似于前缀和:
{
str="abcacwing",str[0]=0(特殊处理),str[1]="a",str[2]="ab",
str[3]="abc",str[4]="abca"...
定义某一个哈希的前缀和:把这个字符串看成是p进制的数
每一位字母(str[i])看成是在p进制下的数字
如果字符串str[4]="abcd",分别在p进制下,a:1,b:2,c:3,d:4
那么str[4]在p进制下的和即为:1*p^3+2*p^2+3*p^1+4*p^0,
这样处理就可以把字符串转化成一个数字
但这个数字可能较大,所以mod(%)上一个数Q,那么就可以把这个大的数字映射成1~Q-1之内的数字了。
接下来通过比较数值大小即可比较两个字串是否相同。
注意:①:不能映射成0,因为0在任何进制下的值都为0,那么字符串:aasxas,axssx,cbdhd都会被映射成0
②:这里是假设不存在冲突,不存在重复的情况。当p=131或者13331,Q=2^64时,基本上不存在冲突
若要模上Q(2^64)那么可以直接用unsigned long long 来存储hash值,溢出的值就是模上2^64的值。
}
这样处理的好处就是可以利用前缀哈希,来确定任意字串的哈希值
{
这里的左边(1)是高位,右边(R)是低位(进制情况下最左边为最低位,即最左边*P^0)。所以在h[R]中R就是第0位,1就是第R-1位,h[l-1]中l-1就是第0位,1就是第l-2位。
那么如果要求L到R之间字串的hash值,那么就先要将h[l-1]的0位与h[R]的第0位对齐
这里R在l的右边,所以改变h[l-1]的0位与h[R]的第0位对齐
(不对齐0位,由于不同数位对应不同P的幂,那将导致相减毫无意义)
比如:1*p^3+2*p^2+3*p^1+4*p^0,数位a对应的是P^3,b对应的是P^2.. 即将h[l-1]*((P^(R-1))/(P^(l-2))
}
也就是达到数位对齐的目的:字符串ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上P的二次方把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。
最终所推得的公式:
子串的hash值为①:h[R]-h[l-1](P^(R-L+1));
所以最终任意字符串的hash值为②:h[i]=h[i-1]*P + str[i];
公式①:
公式②:
题目:841. 字符串哈希 - AcWing题库
代码:
暴力+双指针,样例:8/13:
#include<iostream>using namespace std;int main()
{int m,n;scanf("%d %d",&m,&n);char str[100050];scanf("%s",str);while(n--){int x1,y1,x2,y2;scanf("%d %d %d %d",&x1,&y1,&x2,&y2);bool flag=true;while(x1<=y1 && x2<=y2){if(str[x1-1]!=str[x2-1] ||str[y1-1] != str[y2-1]){flag=false;break;}else x1++,x2++,y1--,y2--;}if(flag) printf("Yes\n");else printf("No\n");}
}
哈希字符串:
#include<bits/stdc++.h>using namespace std;typedef unsigned long long ULL;
const int N=1e5+10,P=131;int n,m;
char str[N];
//p[]是对进制P的预处理,记录了P的不同幂次方
//h[]存放的是该字符串的每一个字母对应的hash值
ULL h[N],p[N];int get(int l,int r)
{return h[r]-h[l-1]*p[r-l+1];
}
int main()
{scanf("%d%d%s",&n,&m,str+1);p[0]=1;for(int i=1;i<=n;i++){
//这里需要多次运用到P的幂次方,所以在此提前做预处理//预处理,这里溢出值相当于取模后的值p[i]=p[i-1]*P;//转换成P进制下的数h[i]=h[i-1]*P+str[i];}while(m--){int l1,r1,l2,r2;scanf("%d%d%d%d",&l1,&r1,&l2,&r2);if(get(l1,r1)==get(l2,r2)) printf("Yes\n");else printf("No\n");}return 0;}
tips:
注意上述所给出的一些小技巧,能更好的帮助理解。
而且比如:p[i]=p[i-1]*P;很好的避免了pow(P,i)的繁杂操作
倘若还是不理解就先将公式记下来