哈希表详细讲解
内容部分引用 oi-wiki
哈希表
引入
哈希表又称散列表,一种以「key-value」形式存储数据的数据结构.所谓以「key-value」形式存储数据,是指任意的键值 key 都唯一对应到内存中的某个位置.只需要输入查找的键值,就可以快速地找到其对应的 value.可以把哈希表理解为一种高级的数组,这种数组的下标可以是很大的整数,浮点数,字符串甚至结构体.
哈希函数
要让键值对应到内存中的位置,就要为键值计算索引,也就是计算这个数据应该放到哪里.这个根据键值计算索引的函数就叫做哈希函数,也称散列函数.在实际的应用中,键值可能是更复杂的东西,比如浮点数、字符串、结构体等,这时候就要根据具体情况设计合适的哈希函数.哈希函数应当易于计算,并且尽量使计算出来的索引均匀分布.
在 OI 中,最常见的情况应该是键值为整数的情况.当键值的范围比较小的时候,可以直接把键值作为数组的下标,但当键值的范围比较大,比如以 \(10^9\) 范围内的整数作为键值的时候,就需要用到哈希表.一般把键值模一个较大的质数作为索引,也就是取 \(f(x) = x \ mod \space m\) 作为哈希函数.
另一种比较常见的情况是 key 为字符串的情况,由于不支持以字符串作为数组下标,并且将字符串转化成数字存储也可以避免多次进行字符串比较.所以在 OI 中,一般不直接把字符串作为键值,而是先算出字符串的哈希值,再把其哈希值作为键值插入到哈希表里.关于字符串的哈希值,我们一般采用进制的思想,将字符串想象成一个 \(127\) 进制的数.那么,对于每一个长度为 \(n\) 的字符串 \(s\),就有:
我们将 \(hash\) 数组定义为 unsigned long long 类型,unsigned long long 的自然溢出就相当于对 \(2^{64}\) 取模,为了防止出题人卡 unsigned long long 自然溢出,可以在预处理 \(hash\) 数组中模上一个大质数(例如:998244353)
查询
当我们需要查询一段字符串的哈希值时,假设这段区间的右端点为 \(r\),左端点为 \(l\),那么它的哈希值即为:
我知道你没看懂,现在举个例子:
例如 \(114514\) 这个数,我们现在要求第 \(2\) 位至第 \(4\) 位的数,显然答案为 \(1145 - 1000 = 145\)
此时发现答案即为 \(hash_4 - hash_1 * base^{3}\)
所以得出,对于区间 \(l,r\),子串的哈希值即为 \(hash[r] - hash[l-1] * base[r-l+1];\)
哈希冲突
我们希望对于任意的键值,哈希函数计算出的索引都不相同,那么我们直接根据索引进行访问即可。但事实上,常常出现两个不同的键值,它们经过哈希函数计算出来的索引是相同的,我们将这种情况称之为 “哈希冲突”。
拉链法
为了解决哈希冲突,OI 中最常用的方法就是拉链法,拉链法的核心思想即为:用数组模拟链表,如果有多个键值经哈希函数计算后的索引相同时,就将它们的信息放到链表中。查询的时候只需把对应索引的链表查询一遍即可。
开放寻址法
开放寻址法,也是一种用于处理哈希冲突的常见方法,开放寻址法的核心思想即为:寻找哈希表中的下一个空位,然后直接把元素扔到这个空位当中。
例题
一.洛谷P11615【模板】哈希表
哈希表的板子题,给出两种做法的代码:拉链法,开放寻址法
1.拉链法
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int T=1e7+19;
const int N=5e6+5;
int h[T],ne[N],idx=0,n;
ull key[N],val[N],x,y,ans;
struct hash{
inline void init(){
memset(h,-1,sizeof(h));
}
ull find(ull x){
int k=(int)(x%T);
for(int i=h[k];i!=-1;i=ne[i]){
if (key[i]==x){
return val[i];
}
}
return 0;
}
inline void insert(ull x,ull y){
int k=(int)(x%T);
for(int i=h[k];i!=-1;i=ne[i]){
if (key[i]==x){
val[i]=y;
return;
}
}
key[idx]=x;
val[idx]=y;
ne[idx]=h[k];
h[k]=idx++;
}
}hh;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
hh.init();
for(int i=1;i<=n;i++){
cin>>x>>y;
ans+=(ull)i*hh.find(x);
hh.insert(x,y);
}
cout<<ans;
return 0;
}
2.开放寻址法
#include<bits/stdc++.h>
using namespace std;
#define int unsigned long long
const int N=1e7+19;
int t,x,y,ans,i=1;
struct hash_table{
int h[N],val[N];
bool used[N];
inline void init(){
memset(used,0,sizeof(used));
return;
}
inline int find(int x){
int k=x%N;
while(used[k] and h[k]!=x){
k++;
if (k==N)k=0;
}
return k;
}
inline int query(int x){
int k=find(x);
return used[k] ? val[k] : 0;
}
inline void insert(int x,int y){
int k=find(x);
used[k]=1;
h[k]=x;
val[k]=y;
return;
}
}hs;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>t;
hs.init();
while(t--){
cin>>x>>y;
ans+=i*hs.query(x);
hs.insert(x,y);
i++;
}
cout<<ans;
return 0;
}
二.洛谷P10468 兔子与兔子
字符串哈希的板子题。
温馨提示:本题不卡自然溢出
#include<bits/stdc++.h>
using namespace std;
#define int unsigned long long
const int N=1e6+5;
int mi[N],hs[N],m,len;
string s;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>s;
int len=s.size();
s=" "+s;
mi[0]=1;
for(int i=1;i<=len;i++){
mi[i]=mi[i-1]*127;
hs[i]=hs[i-1]*127+s[i];
}
cin>>m;
while(m--){
int l1,l2,r1,r2;
cin>>l1>>r1>>l2>>r2;
int key1=hs[r1]-hs[l1-1]*(mi[r1-l1+1]);
int key2=hs[r2]-hs[l2-1]*(mi[r2-l2+1]);
if (key1==key2){
puts("Yes");
}else{
puts("No");
}
}
return 0;
}

浙公网安备 33010602011771号