浅谈hash

来源:互联网 发布:大数据分析和搜索引擎 编辑:程序博客网 时间:2024/06/05 17:08

Hash:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
hash加快查找速度,可以用作查找数值和字符串,以下就拿存储数值为例:
背景:按顺序输入n个数Ai,输出直接输出第i个数是第几次出现的(n<=10^5 Ai<=10^8)题目强制用在线算法(意思就是不能离散化,排序,你求出第i个数第几次出现,才知道第i+1个数是多少)。对于每一个数,我们都可以将他mod一个数(MOD)。比如说选的MOD是7,输入的n个数为:5,3,12,19,12。
i=1:因为5%7=5,发现hash表中下标为5的单元为空,所以直接把5放入位置(5%7=5)当中。把count+1,输出count
count数组(从0开始):0 0 0 0 0 1 0
hash数组(从0开始):0 0 0 0 0 5 0
i=2: 因为3%7=3,发现hash表中下标为3的单元为空,所以直接把3放入位置(3%7=3)当中。把count+1,输出count
count数组(从0开始):0 0 0 1 0 1 0
hash数组(从0开始):0 0 0 3 0 5 0
i=3:因为12%7=5,发现hash表中的下标为5的单元有人了(撞车了),在hash中,这称为冲突,我们就先用最简单的方法,发现有人了,就往下面走,看到下标为6的没人,那么就把12放进6中,做一样的操作。
count数组(从0开始):0 0 0 1 0 1 1
hash数组(从0开始):0 0 0 3 0 5 12
i=4:因为19%7=5,发现hash表中的下标为5的单元有人了,那么就按照老方法,继续往下走,发现还有,可是这时候不能往下走了,因为已经到了数组边界了,我们就从头开始找,找到第0个,发现没人,就可以放进去。
count数组(从0开始):1 0 0 1 0 1 1
hash数组(从0开始):19 0 0 3 0 5 12
i=3:因为12%7=5,发现hash表中的下标为5的单元有人了,就往下面走,看到下标为6的虽然有人,但是这个数是自己,所以这时候直接把count+1,然后输出就好了
count数组(从0开始):1 0 0 1 0 1 2
hash数组(从0开始):19 0 0 3 0 5 12

然后这道题目就算完成了。然后肯定会有疑问,MOD应该选什么,MOD肯定是选质数最好,因为这样会减少很多冲突,这是可以证明的。那么MOD应该选什么质数呢,首先,MOD肯定要比n大,不然一定会有重复。但是MOD也不能太大,因为开数组时大小是根据MOD来开的,会影响空间。所以我推荐选的是[2*n,5*n]以内的质数,既可以减少冲突,还可以只用较少的空间。
再讲讲解决冲突的问题:
遇到冲突,可以有以下解决方法:
1.线性探测法:最简单的,遇到冲突就往下走,但是如果你RP不好的话,你每次都会有冲突,然后时间复杂度高达O(n²)。
2.线性补偿探测法:因为你每次加1可能会遇到很多冲突,可以把每次加1,改成每次加Q,Q可以是在你查找一个数x的时候,Q=x%(一个质数)。这样冲突会减少一点。
3.拉链法:用一个vector或者数组模拟指针,直接还是存在那个位置,只是用一个链表存着,这样不会堆积,时间复杂度也还不错,通常可以达到O(n),是我个人觉得很不错的方法。
4.随机探测法:将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避免或减少堆聚。
5.还有很多种方法,例如平方折中法,二次探测法,平方取余法。。。。。
当然,除了插入一个数的操作,还有删除,线性探测法和线性补偿探测法以及随机探测法删除都很麻烦,而拉链法比较容易,直接把vector或链表中的那个节点删去就可以了。

例题:POJ 1840:传送门
题目大意:有一个方程:a1*x1+a2*x2+a3*x3+a4*x4+a5*x5=0。(-50<=ai<=50且ai为整数题目给出a1,a2,a3,a4,a5,问这个方程有多少组整数解。(xi!=0且-50<=xi<=50)
做法1:首先想到,暴力枚举,枚举5个数就可以。时间复杂度:O(100^5)
做法2:然后可以优化,枚举四个数。先用一个表记住每个数根号三次方是否有整数结果。时间复杂度O(100^4)
做法3:枚举三个数,用hash保存后两个数枚举x4与x5可产生的所有结果。然后枚举三个数,要结果等于0,所以后两个数的结果必须要等于前三个数的相反数,在用hash看看这个数组成有多少种方案,然后ans计数。时间复杂度O(100^3)
做法3的代码:(我用的是线性补偿探测法。)
#include<iostream>#include<cstdio>#include<algorithm>#include<cstring>#include<cstdlib>#include<cmath>#define MOD 23333#define STEP 233using namespace std;struct data{int num;int count;data(){num=0;count=0;}};data hash[MOD+2];int find(int x,int gg){x+=125000001;int y=x%MOD;while(hash[y].count!=0&&hash[y].num!=x){y+=x%STEP+1;if(y>=MOD)y-=MOD;}hash[y].num=x;hash[y].count+=gg;return hash[y].count;}int main(){int a1,a2,a3,a4,a5,k;
long long ans=0;scanf("%d%d%d%d%d",&a1,&a2,&a3,&a4,&a5);for(int i=-50;i<=50;i++)  for(int j=-50;j<=50;j++)if(i!=0&&j!=0)k=find(a4*i*i*i+a5*j*j*j,1);for(int i=-50;i<=50;i++)for(int j=-50;j<=50;j++)for(int k=-50;k<=50;k++)if(i!=0&&j!=0&&k!=0)  {  int sum=-(a1*i*i*i+a2*j*j*j+a3*k*k*k);  ans+=find(sum,0);  }printf("%d",ans);return 0;}


例题2:基因测试:
题目描述:现代的生物基因测试已经很流行了。现在要测试色盲的基因组。有N个色盲的人和N个非色盲的人参与测试。
基因组包含M位基因,编号1至M。每一位基因都可以用一个字符来表示,这个字符是‘A’、'C'、'G'、'T'四个字符之一。
例如: N = 3, M = 8。
色盲者1的8位基因组是: AATCCCAT
色盲者2的8位基因组是: ACTTGCAA
色盲者3的8位基因组是: GGTCGCAA
正常者1的8位基因组是: ACTCCCAG
正常者2的8位基因组是: ACTCGCAT
正常者3的8位基因组是: ACTTCCAT
通过认真观察研究,生物学家发现,有时候可能通过特定的连续几位基因,就能区分开是正常者还是色盲者。
例如上面的例子,不需要8位基因,只需要看其中连续的4位基因就可以判定是正常者还是色盲者,这4位基因编号分别是:
(第2、3、4、5)。也就是说,只需要看第2,3,4,5这四位连续的基因,就能判定该人是正常者还是色盲者。
比如:第2,3,4,5这四位基因如果是GTCG,那么该人一定是色盲者。
生物学家希望用最少的连续若干位基因就可以区别出正常者和色盲者,输出满足要求的连续基因的最少位数。
输入格式 1810.in
第一行,两个整数: N和M。 1 <= N <= 500, 3 <= M <= 500。
接下来有N行,每一行是一个长度为M的字符串,第i行表示第i位色盲者的基因组。
接下来又有N行,每一行是一个长度为M的字符串,第i行表示第i位正常者的基因组。
输出格式 1810.out
一个整数。
输入样例 1810.in
3 8
AATCCCAT
ACTTGCAA
GGTCGCAA
ACTCCCAG
ACTCGCAT
ACTTCCAT
输出样例 1810.out
4
题目可能有点难看懂,先来解释以下题目的意思:就是让一个大小为j的区间,然后色盲者跟非色盲者的基因组都不相同(色盲这跟色盲者相同没事,非色盲者同理)。求一个最小的j。
那么可以看出来,这题应该是哈希,把色盲者跟非色盲者的基因用hash存起来。这时候就要讲到字符串的hash存储了。
先举个例子:
字符串s:dabababac,我们要存储一个这样的字符串,我们可以把他看成相应的进制。
他的hash值可以是 for(int i=0;i<len;i++)key=(key*29)%MOD+s[i];
最后的key就是他的hash值。
可是如果要求字符串的所有长度为j的字符串区间的第hash值呢。
可能很快就会想到说分别枚举,这太慢了。我们用一个类似前缀和的数组,存住一个字符串前i个字符串的hash值。
for(int j=0;j<len;j++)sum[j]=(sum[j-1]*29LL+s[j])%MOD;
然后可以用o(1)的时间算出每个区间的hash值:key=(sum[i+j-1]-(sum[i-1]*p%MOD)+MOD)%MOD;
其中j表示区间长度,i表示起点,p表示29^j。(以上所有29都只是一个较好的质数,可替换其他)
字符串hash通常来说,你都不会比较字符串(因为字符串太长了),如果hash值相等,就直接把字符串当作是相等的了。
有人会说,如果hash值相等,那么字符串也未必相等,的确是这样。但是这样的几率非常小。
如果你觉得单hash有可能你RP太好,真的出现hash值相等,字符串不相等,可以写双哈希,或者随机判断几个字符,这都是可以的。
既然知道了区间的hash值怎么求,这题也差不多了。
但是发现如果单单枚举答案,hash判断的话,时间复杂度是O(n³)是会超时的。
发现答案是有一个性质的:如果答案j不能达成,那么答案i(j>i)也不能达成,这就可以让我们二分答案。
时间复杂度就是O(n²log n)并不会超时。
附上代码:(这题有点特殊,只有4个字母,所以把ASCII码都换成了1,2,3,4,于是进制那里也只是乘了个5
#include<iostream>#include<cstdio>#include<algorithm>#include<cmath>#include<cstdlib>#include<cstring>#define MOD 1000007LLusing namespace std;bool hash[1000007+3];int n,m;long long sum[520][520];long long sum1[520][520];int fj[520][520];int fj1[520][520];char ch1;bool find(long long x,int gg){if(hash[x])return true;if(gg==1)hash[x]=true;return false;}int ch(char c){if(c=='A')return 1;if(c=='C')return 2;if(c=='G')return 3;if(c=='T')return 4;}bool check(int j){long long pp=1;for(int i=0;i<j;i++)pp=pp*5LL%MOD;for(int i=1;i<=m-j+1;i++){memset(hash,0,sizeof(hash));bool bo=false;for(int k=1;k<=n;k++){long long f=(sum[k][i+j-1]-(sum[k][i-1]*pp%MOD)+MOD)%MOD;bool b=find(f,1);}for(int k=1;k<=n;k++){long long f=(sum1[k][i+j-1]-(sum1[k][i-1]*pp%MOD)+MOD)%MOD;if(find(f,0)){bo=true;break;}}if(!bo)return true;}return false;}int main(){scanf("%d%d\n",&n,&m);for(int i=1;i<=n;i++){for(int j=0;j<m;j++){ch1=getchar();fj[i][j+1]=ch(ch1);}ch1=getchar();}for(int i=1;i<=n;i++){for(int j=0;j<m;j++){ch1=getchar();fj1[i][j+1]=ch(ch1);}ch1=getchar();}for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)sum[i][j]=(sum[i][j-1]*5LL+fj[i][j])%MOD;  //计算前i个字符串的hash值for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)sum1[i][j]=(sum1[i][j-1]*5LL+fj1[i][j])%MOD;int l=1,r=m;while(l+1<r)//二分答案{int mid=(l+r)>>1;if(check(mid))r=mid;else l=mid;}printf("%d",r);return 0;}

讲完了字符串,在来讲讲一些坐标的存储。
对于一个二维的坐标,其实并不需要两个hash,只需要存储其中的x,或y就行了。然后再讲讲平方取余法:key=x²%MOD
样的平方取余法是非常好的一个方法,他的冲突非常少。
例题:POJ2002 传送门
题目大意:在一个二维平面内,给你n(n<=1000)个点,Xi,Yi<=20000且都为整数,且没有点重合,求有多少个正方形(斜的也算)。
做法1:枚举四个点,判断这四个点是否形成正方形,非常简单。时间复杂度O(n^4)
做法2:枚举两个点,因为要的是正方形,所以是可以通过数学直接求出来剩下两个点的。数学公式如下:

x3=x[i]+y[i]-y[j],y3=y[i]-x[i]+x[j];  //x[i],y[i]为第一个点   

x4=x[j]+y[i]-y[j],y4=y[j]-x[i]+x[j];  //x[j],y[j]为第二个点。

x3=x[i]-y[i]+y[j],y3=y[i]+x[i]-x[j];

x4=x[j]-y[i]+y[j],y4=y[j]+x[i]-x[j];

证明要用全等三角形,我一个初一蒟蒻证明还不算太懂,这里就不写了。如果真的像看,可以去看我朋友的证明:传送门
或许有兴趣的可以自己推一下。
然后把所有点都用hash存起来,枚举两个点,那么剩下两个点用hash看看是否存在就行。
答案的话,按照上面的方法一个正方形会被算4遍,所以ans最后要除以4
附上代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<vector>
#include<queue>
#define weiyi 20003   //因为有负数的问题,所以每一个数都要加一个数值,变成正数。
#define MOD 100007
#define Maxn 2020
using namespace std;
struct data{
int x;
int y;
data(int a,int b)
{
x=a;
y=b;
}
};
int x[Maxn],y[Maxn];
vector<data> hash[MOD+3];
bool find(int x,int y,int gg)
{
long long x1=(long long)(0LL+x+weiyi)*(0LL+x+weiyi)%(MOD+0LL);//平方取余法
int len=hash[x1].size();
for(int i=0;i<len;i++)//拉链法解决冲突
{
if(hash[x1][i].x==x&&hash[x1][i].y==y)
return true;
}
if(gg==1)hash[x1].push_back(data(x,y));
return false;
}
void init()
{
for(int i=0;i<=MOD;i++)hash[i].clear();
}
int main()
{
freopen("1811.in","r",stdin);
freopen("1811.out","w",stdout);
int n;
scanf("%d",&n);
while(n!=0)
{
int ans=0;
init();
for(int i=1;i<=n;i++)
{
scanf("%d%d",&x[i],&y[i]);
bool t=find(x[i],y[i],1);
}
for(int i=1;i<n;i++)
for(int j=i+1;j<=n;j++)//枚举两个点,通过公式求出其他两个点,全等三角形证明 
{
int x3=x[i]+y[i]-y[j],y3=y[i]-x[i]+x[j];    //神奇的数学公式  
int x4=x[j]+y[i]-y[j],y4=y[j]-x[i]+x[j];
if(find(x3,y3,0)&&find(x4,y4,0))ans++;
x3=x[i]-y[i]+y[j],y3=y[i]+x[i]-x[j];
x4=x[j]-y[i]+y[j],y4=y[j]+x[i]-x[j];
if(find(x3,y3,0)&&find(x4,y4,0))ans++;
}
printf("%d\n",ans/4);  //每个正方形都被算了4遍。 
scanf("%d",&n);
}
return 0;
}

1 0
原创粉丝点击