哈希表
何为哈希表
散列表又称 哈希表,是一种以 key:value 形式存储数据的数据结构。所谓 key:value 就是 键值对,这种数据结构有一个很大的优势:给定一个 key 它能够在 value 找出来。
为了达到这种效果,哈希表使用到了一种特殊的函数 哈希函数 也称 散列函数。key:value 在内存中的地址或是索引。
哈希碰撞
如果对于任意的键值,哈希函数计算出来的索引都不相同,那只用根据索引把 key:value 放到对应的位置就行了。但实际上,常常会出现两个不同的键值,他们用哈希函数计算出来的索引是相同的,这就被称为 哈希碰撞。这时候就需要一些方法来处理冲突。在 OI 中,最常用的方法是 拉链法 和 开放寻址法。
示例题目
维护一个集合,支持如下几种操作:
I x,插入一个整数; Q x,询问整数是否在集合中出现过;
现在要进行
输入格式
第一行包含整数 I x,Q x 中的一种。
输出格式
对于每个询问指令 Q x,输出一个询问结果,如果 Yes,否则输出 No。每个结果占一行。
数据范围
输入样例
5
I 1
I 2
I 3
Q 2
Q 5输出样例
Yes
No拉链法
#include <bits/stdc++.h>
using namespace std;
const int N = 100003;
int h[N], e[N], ne[N], idx;
// 哈希函数
int mhash(int x) {
return (x % N + N) % N;
}
void insert(int x) {
int k = mhash(x);
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool find(int x) {
int k = mhash(x);
for (int i = h[k]; i != -1; i = ne[i]) {
if (e[i] == x) return true;
}
return false;
}
int main() {
int n; cin >> n;
memset(h, -1, sizeof h);
while (n --) {
char op[2];
int x;
scanf("%s%d", op, &x);
if (op[0] == 'I') {
insert(x);
} else {
if (find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}开放寻址法
#include <bits/stdc++.h>
using namespace std;
const int N = 200003, INF = 0x3f3f3f3f; // INF 表示该位置没有值
int h[N];
// 哈希函数
int mhash(int x) {
return (x % N + N) % N;
}
int find(int x) { // 如果能找到 x 则返回 x 的索引,否则返回 x 应该存储的位置
int k = mhash(x);
while (h[k] != INF && h[k] != x) { // 该位置有值且值不是 x
k++;
if (k == N) k = 0;
}
return k;
}
int main() {
int n; cin >> n;
memset(h, 0x3f, sizeof h); // 初始化所有位置为空
while (n --) {
char op[2];
int x;
scanf("%s%d", op, &x);
int k = find(x);
if (op[0] == 'I') {
h[k] = x;
} else {
if (h[k] != INF) puts("Yes");
else puts("No");
}
}
return 0;
}字符串哈希
如果 key 是字符串,由于不支持以字符串作为数组下标,并且将字符串转化成数字存储也可以避免多次进行字符串比较。所以在 OI 中,一般不直接把字符串作为键值,而是先算出字符串的哈希值,再把其哈希值作为键值插入到哈希表里。关于字符串的哈希值,我们一般采用进制的思想,将字符串想象成一个
我们可以将得到的 unsigned long long 的最大值)取模。通常的做法是将 unsigned long long,这样 unsigned long long 的自然溢出就等价于取模操作了。可以使操作更加方便。
这种方法虽然简单,但并不是完美的。可以构造数据使这种方法发生冲突(即两个字符串的
实际应用
字符串前缀哈希
设有字符串
针对上述哈希函数有两点特殊说明:
- 不存在冲突
两个操作:
- 预处理字符串:
- 计算任意子串
的哈希值:
示例题目
给定一个长度为
输入格式
第一行包含整数
注意,字符串的位置从
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No。每个结果占一行。
数据范围
输入样例
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2输出样例
Yes
No
Yes参考代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5 + 10, P = 131;
char s[N];
ULL h[N], p[N];
int n, m;
// 求区间 [l, r] 的哈希值
ULL get(int l, int r) {
return h[r] - h[l - 1] * p[r - l + 1];
}
int main() {
scanf("%d%d%s", &n, &m, s + 1);
// 预处理 p^i 以及 h[i]
p[0] = 1;
for (int i = 1; i <= n; i++) {
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + s[i];
}
while (m--) {
int l1, r1, l2, r2;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
if (get(l1, r1) == get(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}习题
- 洛谷 P4305. 不重复数字
离散化
AcWing 804 区间和
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。现在,我们首先进行
输入格式
第一行包含两个整数
输出格式
共
数据范围
输入样例
3 3
1 2
3 6
7 5
1 3
4 6
7 8输出样例
8
0
5参考代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int const N = 3e5 + 10;
int n, m, a[N], s[N];
typedef pair<int, int> PT;
vector<int> alls;
vector<PT> add, query;
// 二分查找
int find(int x) {
int l = 0, r = alls.size() - 1;
while(l < r) {
int mid = l + r >> 1;
if(alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
int main() {
cin >> n >> m;
// 输入所有的 x c
for(int i = 0; i < n; i++) {
int x, c;
cin >> x >> c;
add.push_back({x, c});
alls.push_back(x);
}
for(int i = 0; i < m; i++) {
int l, r;
cin >> l >> r;
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
// 去除重复的下标
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
// 处理
for(auto item : add) {
int x = find(item.first);
a[x] += item.second;
}
// 预处理前缀和
for(int i = 1; i <= alls.size(); i++) {
s[i] = s[i - 1] + a[i];
}
for(auto item : query) {
int l = find(item.first);
int r = find(item.second);
cout << s[r] - s[l - 1] << endl;
}
return 0;
}