目录
一,整数哈希
整数哈希主要有两种方法:拉链法与开放寻址法。
什么时候会用到哈希呢?
当数据范围在1e-9到1e9时,需要我们查询每个数据出现的次数或者一个数是否出现时就可以用哈希表
1,拉链法
拉链法是通过数组和单链表的形式对整数进行存储,我们用一个长度为N的数组来存储数据,对于每个数,我们都将他取模数N,这样每个数都可以落在0~N-1的区间上,但是可能会出现哈希冲突,何为哈希冲突,就是可能会有两个数取模后他们的余数相同,即这两个数会落在同一个区间,那么如何解决这种问题呢,我们可以以这个余数作为头结点,建立一个单链表,即这种结构:
这样我们对于查询每个数,先得到这个数的余数,然后再遍历以这个数为头结点的链表,进行查找操作。
那么还有一个问题:我们如何确定数组N取多少合适呢?
这个一般依据题意来设定,例如题目需要我们在数据范围为1e-9到1e9的情况下循环1e5个数是否出现或者出现的次数时,就可以将N设定为大于1e5的质数。
代码如下:
哈希表 拉链法,数组加单链表
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
//N的取值尽量是一个大于题目要求且是比较大的质数,这样可以让我们取模后落在同一个数的可能尽可能的小
const int N = 10003;
int h[N], e[N], ne[N], idx;
void insert(int x)
{
//让负数取模后落在正数区间上
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool find(int x)
{
int k = (x % N + N) % N;
//遍历单链表
for (int i = h[k]; i != -1; i = ne[i])
{
if (e[i] == x)
return true;
}
return false;
}
int main()
{
int n;
scanf("%d", &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;
}
2,开放寻址法
开放寻址法的原理就类似于我们去厕所找坑位,当一个坑位有人时我们就去下一个坑位,即如果一个数取模后这个位置上面已经有其他的数取模了就将这个数往后存储,直到找到这个下标为空的位置填入,因此当我们用开放寻址法时我们要开题意要求的两到三倍的区间,防止哈希冲突
代码如下:
//哈希表 开放寻址法
#include<iostream>
#include<cstring>
using namespace std;
//开放寻址法类似与去厕所找坑位,用一个一维数组模拟坑位,如果这个坑位有人,就k++,所以开的数组长度一般为题目要求的两到三倍的质数
const int N = 20003,null=0x3f3f3f3f;
int h[N];
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;
}
int main()
{
int n;
scanf("%d", &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] != null)
puts("Yes");
else
puts("No");
}
}
return 0;
}
二,字符串哈希
字符串哈希的运用非常广泛。常见的有让我们求一个字符串中两个子串是否相等或者求多个字符串中,有多少个不相同的字符串等等,运用很多,哈希是处理字符串的一把利器,所以需要掌握其中的原理。
字符串哈希的核心思想就是将一个字符串转换成数字进行存储。
对于每个字符串,我们可以将其看成一串P进制的数组例如,我们可以将字符串ABCD看成P进制下的1 2 3 4,转换成十进制就是1*P^3+2*P^2+3*P^1+4*P^0,假设P为8,那么这个数就是668,那么668这个数就对应着ABCD这个字符串,将任意两个字符串转换成数字后进行比较的话就非常方便迅速了,可以做到O(1)的时间复杂度,但是,如果当字符串非常长的时候这个数会非常大,所以我们还要确定一个模数MOD将这个字符串转换后的数字在一个长度为0~MOD-1的区间上一一对应。
那么现在的问题就是P和MOD如何取值,一般来说,将P取131或是13331(经验值),MOD取2^64(经验值)时哈希冲突的概率比较小,并且,当我们取2^64为余数时,即unsiged long long溢出时会自动对2^64取模。
接下来就是如何求得一个字符串的子串的哈希值
以h[N]表示以str[N]结尾的字符串的哈希值
根据公式推导可以知道区间L到R区间的哈希值为h[R]-h[L-1]*P^(R-L+1).
同时可以求得h[i]=h[i-1]*P+str[i];
#include<iostream>
#include<algorithm>
using namespace std;
typedef unsigned long long ull;
const int N = 100010, P = 131;
int n, m;
char str[N];
//h数组存储每个字符串的哈希值
//p数组存储进制的N次方
ull h[N], p[N];
//求一个区间的字符子串的哈希值
ull get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
scanf("%d%d%s", &n, &m, str+1);
p[0] = 1;
for (int i = 1; i <= n; i++)
{
//预处理出来p的i次方
p[i] = p[i - 1] * P;
//预处理出来每个前缀子串的哈希值
h[i] = h[i - 1] * P + str[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");
}
}