首先,哈希表的大致作用就是将一个较大值域中的值映射到另一个较小的范围内,这样做最直观的好处就是节省储存空间和减少查询的时间复杂度,鉴于作用对象的不同可以大致分为对整数的“存储结构”和对字符的“字符串哈希方式”。
我们先讨论整数哈希表,其中所用到的核心方法就是对大值域中的值取模,以此让它们来对应一个新的较小的值,但也因此必然会出现多个值映射到同一个值的情况,为此我们有两种方法来帮助我们梳理储存关系:“拉链法”和“开放寻址法”。
一、拉链法:
基本思想:构造出一条空值单链,也就是要投影的“小范围值域”,我称其为主链,当有重复的只出现时,就令它在接在对应主链位置已有值的旁边,形成一条新的支链,后来的重复值依次接在对应支链后面。
主要模板:
#include<iostream>
using namespace std;
const int N=100003;
int h[N],e[N],ne[N],idx;
memset(h,-1,sizeof h);
拉链法中N的取值标准:大于题目所给“小范围”数据的第一个质数,可以使得取模后对应值重复的情况最少。h为主链位置值,e为当前位置储存值。memset(h,-1,sizeof h)作用为将主链函数h中的值都清空。
向主链插入x值:
int k=(x%N+N)%N;
e[idx]=x;
ne[idx]=h[k];
h[k]=idx++;
k为x要插入的主链位置值,(x%N+N)%N是为使k为正数。原理与单链插入相同。
查询x是否出现过:
int k=(x%N+N)%N;
for(int i=h[k];i!=-1;i=ne[i])
if(e[i]==x)
return ture;
return false;
若x出现过,那x一定是在k所在的支链上,因此直接在支链上顺序查找,可以大大减少查找所用的时间。
开放寻址法:
基本思想:开一个数组,在每一个x取模后所在的位置k进行判断,若该处是没有储存过值的空位,或储存的是x的同值,那么就返回k,将x储存于此并得出x的储存位置;如果两种情况都不符合,那就从k开始顺序查找符合的位置,如果到末尾(k==N)也没找到,就再从头(k==0)开始找。
主要模板:
const int N=200003,null=0x33f3f3f;
int h[N];
memset(h,0x3f,sizeof h);
开放寻址法中N的取值标准:大于题目所给“小范围”数据两倍的第一个质数。这里null比10的9次方大,用于规定“空”。
int find(int x)
{
int k=(x%N+N)%N;
while(h[k]!=null&&h[k]!=x)
{
k++;
if(k==N) k=0;
}
return k;
}
插入x:
cin>>x;
int k=find(x);
h[k]=x;
找到符合条件的位置附于x值。
查找x:
int k=find(x);
if(h[k]!=null) puts("Yes");
else puts("No");
判断x应当存在的位置是否为空,不是则代表x被储存过。
---------------------------------------------------------------------------------------------------------------------------------------------
再看字符串,字符串的哈希方法有“字符串前缀哈希法”,其核心思想就是将字符串中每个字符的哈希值以p进制转10进制的方式转化为数进行储存。
主要模板:
#include<iostream>
using namespace std;
typedef unsigned long long ull;
const int N=100010,P=131;
char str[N];
ull h[N],p[N];
cin>>str+1;
p[0]=1;
for(int i=1;i<=n;i++)
{
p[i]=p[i-1]*P;
h[i]=h[i-1]*P+str[i];
}
p作为进制转换所用到的关键值,h[x]表示前x个字符的哈希值。
由此也可以计算字符串中任意一段子链的哈希值:
ull get(int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
有点难懂,需多加记忆。