哈希表
哈希表是一种高效的数据结构。它的优点同字符串哈希一样,查找的算法时间效率几乎就是常数时间,同时也很容易实现,多产生的代价仅仅是消耗内存。
那么什么是哈希表呢 ,我的理解是:按一种分类方式将所有元素分一次类,同一个类别的元素再通过另一种方式存到这个类里
总之就是两次哈希表
假设元素第一次的分类是
D
D
D,第二次的分类是
E
E
E,那么我们就可以用
<
D
,
E
>
<D,E>
<D,E>来存储这一个元素
哈希表就长这个样:
可以说是一个链表
那么我们就可以类比图论中的单向边
存图用
v
e
c
t
o
r
vector
vector和前向星 这里也可以用
哈希函数的构造
哈希函数决定了哈希表查找效率的关键,因为只要哈希值的分布足够平均,单次查找链表的复杂度就会尽量地小。
三种效果较好且较为容易实现的哈希函数:
1.除余法
选择一个适当的正整数
b
b
b,用其对
b
b
b取模的余数作为哈希值,即
H
(
k
e
y
)
=
k
e
y
m
o
d
b
H(key)=key\,\,mod\,\,b
H(key)=keymodb,这个方法我用得最多 (其实不会其他的… ,而且多数情况下性价比最高,关键是
b
b
b的选取。一般选用存储的下的较大的质数(一般情况下根据空间取
1
0
6
10^6
106左右的质数)。这样能尽量避免冲突。
假设
b
=
1000
b=1000
b=1000,哈希函数分类最多才
1000
1000
1000类,冲突很多每次查找的常数就越大。还不如多开点空间,用空间换时间更划算一点。
2.乘积取整法
我们用一个
k
e
y
key
key乘以一个在
(
0
,
1
)
(0,1)
(0,1)中的实数
A
A
A (最好是无理数,
5
−
1
2
\frac{\sqrt{5}-1}{2}
25−1是一个实际效果很好的数),得到一个
(
0
,
k
)
(0,k)
(0,k)之间的实数;取其小数部分,乘以哈希表的大小
M
M
M再向下取整,即得
k
e
y
key
key在
H
a
s
h
Hash
Hash表中的位置。函数表达式可以写成:
H
(
k
e
y
)
=
{
M
(
k
e
y
×
A
m
o
d
1
)
}
H(key)=\{M(key×A\,\,mod1)\}
H(key)={M(key×Amod1)},其中
{
x
}
\{x\}
{x}表示
x
x
x的小数部分
这方法还是可行的 不过还是第一种的简单
3.基数转换法
基数转换法也是采用字符串哈希所用的转换方法:将
k
e
y
key
key值看成是另一种进制的数,然后再把它转换成对应的十进制数,再用除余法对其取余。一般取大于
10
10
10的数作转换的基数,并且两个基数是互质的
如:
k
e
y
=
15539
key=15539
key=15539,现在将它看做是十一进制数
(
15539
)
11
(15539)_{11}
(15539)11,然后将它再转换成十进制数
(
15539
)
11
=
(
21943
)
10
(15539)_{11}=(21943)_{10}
(15539)11=(21943)10
这方法??为啥不直接除余法 怕冲突多常数大? 有这进制转换的功夫还不如直接查了…
其实哈希函数的构造方法多种多样,并没有硬性地规定。只要能尽量地规避冲突,都是可以的。
例题
图书管理
S
o
l
u
t
i
o
n
:
Solution:
Solution:裸哈希表
用俩哈希搞就okk了
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 300300
#define reg register
#define QAQ puts("QAQ");
const int mod1=155339,mod2=155399,p1=15539,p2=93551;
struct node{
int to,nxt;
}edge[N];
int n,cnt,len,sum1,sum2,head[N];
char op[10],s[500];
inline void add(int x, int y){
edge[++cnt].to=y,edge[cnt].nxt=head[x],head[x]=cnt;
}
inline bool query(int x, int y){
for(reg int i=head[x];i;i=edge[i].nxt){
if(y==edge[i].to)return true;
}
return false;
}
int main(){
scanf("%d",&n);
for(reg int i=1;i<=n;i++){
cin>>op;
gets(s);len=strlen(s),sum1=sum2=0;
for(reg int i=0;i<len;i++){
sum1=(sum1*p1+s[i])%mod1;
sum2=(sum2*p2+s[i])%mod2;
}
if(op[0]=='a')add(sum1,sum2);
else {
if(query(sum1,sum2))puts("yes");
else puts("no");
}
}
}
门票
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:
按照模数分类 每个分类后面直接接这个数就行
相当于连一条
n
%
m
o
d
−
>
n
n\%mod->n
n%mod−>n的边
代码
↓
↓
↓
#include<bits/stdc++.h>
using namespace std;
#define reg register
#define int long long
#define N 2000200
const int mod=1553399;
struct node{
int to,nxt;
}edge[N];
int a,b,c,cnt,now,head[N];
inline void add(int u, int v){
edge[++cnt].to=v,edge[cnt].nxt=head[u],head[u]=cnt;
}
inline bool query(int u, int v){
for(reg int i=head[u];i;i=edge[i].nxt){
if(edge[i].to==v)return true;
}
return false;
}
signed main(){
scanf("%lld%lld%lld",&a,&b,&c);
now=1;add(1,1);
for(reg int i=2;i<=2000001;i++){
now=(a*now+now%b)%c;
if(query(now%mod,now)){printf("%lld\n",i-1);return 0;}
add(now%mod,now);
}
puts("-1");
}
收集雪花
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:直接离散化
离散化之后 我们定义一个数组
l
s
t
lst
lst记录每个数上次出现的位置
对于每个数 如果上次出现的位置在我们的区间内 我们直接将左区间调到这个位置加一
在每次操作中都要记得维护
a
n
s
ans
ans哦
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
#define N 1000100
#define reg register
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,ans,a[N],b[N],c[N],lst[N];
int main(){
read(n);
for(reg int i=1;i<=n;i++)read(a[i]),b[i]=a[i];
sort(b+1,b+1+n);
for(reg int i=1;i<=n;i++)c[i]=lower_bound(b+1,b+1+n,a[i])-(b+1);
for(reg int i=1,j=1;i<=n;i++){
if(lst[c[i]]){
if(lst[c[i]]>=j)j=lst[c[i]]+1;
lst[c[i]]=i;
}
else lst[c[i]]=i;
ans=max(ans,i-j+1);
}
printf("%d\n",ans);
}
总结
字符串哈希是一种非常高效的算法,
O
(
1
)
O(1)
O(1)的转移也是非常划算。但有时很容易被卡,也没有一些算法高效,所以用其他方法尽量不用哈希。
哈希表是一种非常实用的数据结构,其哈希函数决定了它的效率。哈希表没什么大毛病,比什么
m
a
p
,
u
n
o
r
d
e
r
e
d
_
m
a
p
,
s
e
t
,
m
u
l
t
i
s
e
t
map,unordered\_map,set,multiset
map,unordered_map,set,multiset常数小多了,也不难打。
P
S
.
PS.
PS.变量名一定不要起
h
a
s
h
hash
hash…