哈希表详细讲解

内容部分引用 oi-wiki

哈希表

引入

哈希表又称散列表,一种以「key-value」形式存储数据的数据结构.所谓以「key-value」形式存储数据,是指任意的键值 key 都唯一对应到内存中的某个位置.只需要输入查找的键值,就可以快速地找到其对应的 value.可以把哈希表理解为一种高级的数组,这种数组的下标可以是很大的整数,浮点数,字符串甚至结构体.

哈希函数

要让键值对应到内存中的位置,就要为键值计算索引,也就是计算这个数据应该放到哪里.这个根据键值计算索引的函数就叫做哈希函数,也称散列函数.在实际的应用中,键值可能是更复杂的东西,比如浮点数、字符串、结构体等,这时候就要根据具体情况设计合适的哈希函数.哈希函数应当易于计算,并且尽量使计算出来的索引均匀分布.

在 OI 中,最常见的情况应该是键值为整数的情况.当键值的范围比较小的时候,可以直接把键值作为数组的下标,但当键值的范围比较大,比如以 \(10^9\) 范围内的整数作为键值的时候,就需要用到哈希表.一般把键值模一个较大的质数作为索引,也就是取 \(f(x) = x \ mod \space m\) 作为哈希函数.

另一种比较常见的情况是 key 为字符串的情况,由于不支持以字符串作为数组下标,并且将字符串转化成数字存储也可以避免多次进行字符串比较.所以在 OI 中,一般不直接把字符串作为键值,而是先算出字符串的哈希值,再把其哈希值作为键值插入到哈希表里.关于字符串的哈希值,我们一般采用进制的思想,将字符串想象成一个 \(127\) 进制的数.那么,对于每一个长度为 \(n\) 的字符串 \(s\),就有:

\hash\[n = hashn-1 * 127 + sn ; \]

我们将 \(hash\) 数组定义为 unsigned long long 类型,unsigned long long 的自然溢出就相当于对 \(2^{64}\) 取模,为了防止出题人卡 unsigned long long 自然溢出,可以在预处理 \(hash\) 数组中模上一个大质数(例如:998244353)

查询

当我们需要查询一段字符串的哈希值时,假设这段区间的右端点为 \(r\),左端点为 \(l\),那么它的哈希值即为:

\key = hash\[r - hashl-1 * baser-l+1; \]

我知道你没看懂,现在举个例子:

例如 \(114514\) 这个数,我们现在要求第 \(2\) 位至第 \(4\) 位的数,显然答案为 \(1145 - 1000 = 145\)

此时发现答案即为 \(hash_4 - hash_1 * base^{3}\)

所以得出,对于区间 \(l,r\),子串的哈希值即为 \(hashr - hashl-1 * baser-l+1;\)

哈希冲突

我们希望对于任意的键值,哈希函数计算出的索引都不相同,那么我们直接根据索引进行访问即可。但事实上,常常出现两个不同的键值,它们经过哈希函数计算出来的索引是相同的,我们将这种情况称之为 "哈希冲突"。

拉链法

为了解决哈希冲突,OI 中最常用的方法就是拉链法,拉链法的核心思想即为:用数组模拟链表,如果有多个键值经哈希函数计算后的索引相同时,就将它们的信息放到链表中。查询的时候只需把对应索引的链表查询一遍即可。

开放寻址法

开放寻址法,也是一种用于处理哈希冲突的常见方法,开放寻址法的核心思想即为:寻找哈希表中的下一个空位,然后直接把元素扔到这个空位当中。

例题

一.洛谷P11615【模板】哈希表

哈希表的板子题,给出两种做法的代码:拉链法,开放寻址法

1.拉链法

cpp 复制代码
#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.开放寻址法

cpp 复制代码
#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 兔子与兔子

字符串哈希的板子题。

温馨提示:本题不卡自然溢出

cpp 复制代码
#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;
}