哈希表(散列表)
一、引言
先来看一道题目:
给定一个数组a含有n个正整数,给定一个数组b含有m个正整数,问b数组中有多少个数在a中出现过?
有一种最朴素的想法是:直接暴力枚举:
for(int i = 0;i<a.size();i++)
{
for(int j = 0;j<b.size();j++)
{
if(a[i]==b[j])
{ cnt++;break;}
}
}//容易知道,这个代码的时间复杂度是O(n^2)的
有没有更快的算法呢?
考虑空间换时间,不放开个数组记录b数组的数字是否出现,然后再去a数组中遍历判断即可,时间复杂度O(n)
bool st[N];
for(int i = 0;i<b.size();i++)
{
st[b[i]] = true;
}
for(int i = 0;i<a.size();i++)
{
if(st[a[i]]) cnt++;
}
容易发现,当数字比较大,超过1e8的时候,数组就会越界了,因为开辟不了这么大的空间,甚至下标不是数字,而是字符串,这种情况是无法先标记再遍历的,那么要引入哈希表的概念。
二、概念
通过引言可以知道,哈希表是为了解决数组越界这类问题的,它能够把值域比较大的数据映射到0~1e5或者1e6的范围。
2.1 概念
引用算法笔记上的原话:hash将元素通过一个函数转换为整数,使得该整数可以尽量唯一地代表这个元素。其中,这个转换函数称为散列函数H,即元素转换前为a,那么转换后的值为H(a)
2.2 方法
一般来说哈希函数直接取模就可以了,但是可能会产生冲突,把两个不一样的数映射成了同一个数。
如何处理冲突?分为下列两个方法:
2.2.1 开放寻址法
只需要一个一维数组,类似上厕所找坑位,如果找的这个坑有人了就看下一个,没人就占坑
- 添加
- 查找
- 删除(找到之后打一个标记)
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+3;//N是大于数据范围的第一个质数
int h[N],n;
const int INF = 0x3f3f3f3f;//INF不在数据范围内,用来初始化
int find(int x)
{
//如果x在hash表中已经存在,返回x在hash表中的位置
//如果不存在,返回x在hash表中可以占的第一个位置
int k = (x%N+N)%N;
while(h[k]!=INF&&h[k]!=x)
{
k++;
if(k==N) k = 0;
}
return k;
}
int main()
{
cin>>n;
memset(h,INF,sizeof h);
while(n--)
{
char op;
cin>>op;
int x;
cin>>x;
int k = find(x);
if(op=='I')
{
h[k] = x;
}
else
{
if(h[k]!=INF) puts("Yes");
else puts("No");
}
}
}
2.2.2 拉链法
很简单的方法:
- 开一个一维数组存储所有的哈希值
- 在每一个数组元素上拉一条链存储映射到这个元素上的所有数
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+3;//大于100000的第一个质数是100003
//这里有点像邻接表
int h[N],e[N],ne[N],idx;
void insert(int x)
{
int k = (x%N+N)%N;//为什么+N%N,目的是让k为正数
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;i = ne[i])
{
if(e[i]==x) return true;
}
return false;
}
int main()
{
memset(h,-1,sizeof h);
int n;
cin>>n;
while(n--)
{
char op;
cin>>op;
int x;cin>>x;
if(op=='I')
{
insert(x);
}
else
{
if(find(x))
{
cout<<"Yes\n";
}
else
{
cout<<"No\n";
}
}
}
}
三、字符串哈希
3.1 字符串前缀哈希法
预处理出所有前缀的哈希值:
如何定义某一个前缀的哈希值?
- 把字符串看成是一个p进制的数
- 比如ABCD,把A当成1,B当成2,C当成3,D当成4,那么 ( A B C D ) p = ( 1234 ) p = ( 1 ∗ p 3 + 2 ∗ p 2 + 3 ∗ p 1 + 4 ∗ p 0 ) m o d Q (ABCD)_p = (1234)_p = \\(1*p^3+2*p^2+3*p^1+4*p^0) mod Q (ABCD)p=(1234)p=(1∗p3+2∗p2+3∗p1+4∗p0)modQ因为可能式子值比较大,所以要取模。
通过上述方式即可以把任何一个字符串映射到从0到Q-1的任何一个数。
注意:
-
不能把字母映射成0,假设把A映射成0,而p进制下0是0,十进制下也是0,所以A是0,AA也是0,就会产生冲突。
-
字符串前缀哈希法依然可能存在冲突,使冲突概率最小的方法有个经验值:
P = 131或13331,Q = 2的64次方
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5,P = 131;
typedef unsigned long long ULL;
ULL h[N],p[N];
int n,m;
string x;
ULL query(int l,int r)
{
return h[r] - h[l-1]*p[r-l+1];
}
int main()
{
cin>>n>>m;
cin>>x;
p[0] = 1,h[0] = 0;
for(int i= 0;i<n;i++)
{
p[i+1] = p[i]*P;
h[i+1] = h[i]*P+x[i];
}
while(m--)
{
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(query(l1,r1)==query(l2,r2)) cout<<"Yes\n";
else cout<<"No\n";
}
return 0;
}
四、树哈希
未完待续~