数值哈希表:
可以使一个庞大的数据空间映射到一个较小的空间范围,比如:将-10^9~10^9的数映射到0~10^5的数
注:哈希表可以把题目O(n)的时间复杂度降为O(1)
链地址法:
1. 拉链法
拉链法本质就是邻接表,将重复的元素连接在一条链上。
槽的大小:这里的槽就是指哈希表的单元格,指的就是 N,一般而言 N取10^5或10^6后面的质数,如1e^5+3或者1e^6+3这种值。
哈希函数:hash ( x ) = ( x % N + N ) % N
插入操作:利用哈希函数得到哈希值 k ,然后确定的这个槽就是 h [ k ]
例题 :
一、题目描述
维护一个集合,支持如下几种操作:
I x,插入一个数 x;
Q x,询问数 x 是否在集合中出现过;
现在要进行 N 次操作,对于每个询问操作输出对应的结果。
输入格式第一行包含整数 N ,表示操作数量。
接下来 N 行,每行包含一个操作指令,操作指令为 I x,Q x 中的一种。
输出格式
对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No。每个结果占一行。
数据范围
1≤N≤10^5
−10^9≤x≤10^9
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:Yes
No
#include<bits/stdc++.h>
#include<cstdio>
using namespace std;
const int N = 10003;
int h[N],e[N],ne[N],idx;//h数组代表槽点,e和ne是链表数组,idx表示头节点的位置
void insert(int x) {
int k = (x % N + N ) % N;//将一个较大的数映射到一个较小的数上
e[idx] = x;//通过链表存储同一个映射的数
ne[idx] = h[k];
h[k] = idx ++;//h[k]存的是链表头节点的下标
}
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);//使用%s输入可以自动去除空格和回车符
if(*op == 'I') {//使用*op指针指向数组的第一个位置
insert(x);
}
else {
if(find(x))
puts("Yes");
else
puts("No");
}
}
return 0;
}
开放寻址法:
处理冲突的思路:
1.开一维数组,一维数组的大小是题目数据的2-3倍
2.添加:从第k个下标开始找,直到找到第一个没有数值的位置插入数据
3.查找:从第k个位置往后找,如果存在数值并且判断数值是否是我们要找的内容
4.删除:不会把数值真的删除,通过创建一个数组标记这个数据是否被删除即可;如设一个bool类型的数组来标记
大部分情况只需要使用添加和查找。
#include<bits/stdc++.h>
#include<cstdio>
using namespace std;
const int N = 200003,null = 0x3f3f3f3f;//0x3f3f3f3f趋近于无穷大
//N为找到第一个大于数据范围的最小质数
//null为映射范围外的数,代表这个位置为数值为空(即约定一个数值代表空)
int h[N];
bool find(int x) {
int k = (x % N + N) % N;
//将大的数值映射到小的数值上并防止出现映射到负数的情况(即将数值映射到数组下标内)
while (h[k] != null && h[k] != x) {
//从第k个位置开始判断,如果此位置不为空且数值不等于查询数值则看下一个位置
k ++;
if (k == N)//判断完最后一个位置,又从0下标开始判断
k = 0;
//因为数组是操作次数的两倍,所以循环一定会停止
}
return k;//如果存在该数值则返回该数值存储的下标,如果该数值不在哈希表中的话k就是存储该数值的位置
}
int main() {
int n;
scanf("%d",&n);
memset(h,0x3f,sizeof(h));//memset是按字节初始化的,int为4个字节,所以等同于0x3f3f3f3f
//将数组内的数值初始化为映射范围外的数值,代表这个位置为空
while(n--) {
char op[2];
int x;
scanf("%s%d",&op, &x); //使用%s输入可以自动去除空格和回车符
int k = find(x);
if(*op == 'I') {//使用*op指针指向数组的第一个位置
h[k] = x;
} else {
if(h[k] != null)//当前数值不为约定的空值时,就代表找到了需要找的数值
puts("Yes");
else
puts("No");
}
}
return 0;
}
注:
memset函数是按照字节来初始化的
memset(数组,0,sizeof())代表将int数组上的每一个字节都初始化为0,二进制都为0的数是0
memset(数组,-1,sizeof())代表将int数组上的每一个字节都初始化为1,二进制都为1的数是-1
memset(数组,0x3f,sizeof())代表将int数组上的每一个字节都初始化为0x3f,即等于0x3f3f3f3f
字符串前缀哈希法(很厉害的算法,必须掌握):
当我们需要快速判断两个字符串是否相等时就可以用这种方法做题;KMP可以用来求循环节,这个方法不能求循环节
字符串前缀哈希法:
1.先预处理出来所有前缀的哈希,如h[0]代表前0个字符的哈希值;h[1]表示前1个字符的哈希值;h[2]表示前2个字符的哈希值;
如何定义某一个前缀的哈希值:把字符串看成是P进制的数,每一位字母代表P进制数的每一位数字;如”ABCD”,且A=1,B=2……Z=26,则(ABCD)p对应的十进制数为(1*P^3+2^P^2+3*P^1+4*P^0)modQ,因为可能字符串很长,所以转换后的十进制数可能很大,所以mod上一个比较小的数,通过取mod就能把任何一个字符串映射到0~Q-1上的数了
2.不要把某个字母映射成0
3.假设不存在冲突的情况下使用,因为当我们的P取131或13331,Q取2^64次方时99.99%的情况下不会发生冲突的
注:模2^64次幂可以使用unsigned long long 定义h数组,当数值溢出时就可以看为是取mod了(处理溢出机制是通过取模来完成的)
4.好处:可以利用前缀哈希将所有子串的哈希都求出来
主要思想
首先,想求l~r这段的哈希值且已知h[l-1]和h[r]的哈希值;因为是P进制数,所以左边为高位,右边为低位,即h[r]为P进制数的第0位,h[1]为P进制数的第r-1位 ,h[l-1]为P进制数的第0位,h[1]为P进制数的第l-2位
其次,我们令h[l-1]乘以P^r-l+1,使h[l-1]的P进制数向左移动若干位置,与h[r]的P进制数位置对齐
最后,令两者的值相减,就可以得到某一段字符串长度的哈希值了
公式:h[r]-h[l-1]*P^r-l+1
预处理前缀哈希值:h[i]=h[i-1]*P+str[i]
当预处理完前缀子串的数值后就可以使用O(1)的时间算出某一字串的哈希值了
例题:
字符串哈希
给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数 l1,r1,l2,r2 ,请你判断[ l1,r1 ]和[ l2,r2 ]这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数n和m,表示字符串长度和询问次数。
第二行包含一个长度为n的字符串,字符串中只包含大小写英文字母和数字。
接下来m行,每行包含四个整数 l1,r1,l2,r2 ,表示一次询问所涉及的两个区间。
注意,字符串的位置从1开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围
1 ≤ n, m ≤ 10^5
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2输出样例:
Yes
No
Yes
#include<cstdio>
#include<iostream>
using namespace std;
typedef unsigned long long ULL;
//将无符号长整型类型名定义成ULL,长整型的溢出机制是通过对2^64取模来进行的
const int N = 100010, P = 131;
int n,m;
char str[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);
//str+1代表从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];//预处理字符串前缀哈希值
}
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;
}