一本通提高篇 哈希和哈希表 (二)哈希表

哈希表

哈希表是一种搞笑的数奆结垢。它的优点同字符串哈希一样,查找的算法时间效率几乎就是常数时间,同时也很容易实现,多产生的代价仅仅是消耗内存。

那么什么是哈希表呢 ,我的理解是:按一种分类方式将所有元素分一次类,同一个类别的元素再通过另一种方式存到这个类里
总之就是两次哈希表
假设元素第一次的分类是 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
这方法??为啥不直接除余法 怕冲突多常数大? 有这进制转换的功夫还不如直接查了…

其实哈希函数的构造方法多种多样,并没有硬性地规定。只要能尽量地规避冲突,都是可以的。

b b bb bb 了这么多 上题吧 ↓ ↓

题目描述
图书管理是一-件十分繁杂的工作,在一个图书馆中每天都会有许多多新书加入,为了更方便地管理图书(以便F帮助想要借书的客人快速查找是否有他们所需要的书),我们需要设计一个图书查找系统。
这个系统需要支持2种操作:
add(s),表示新加人一本书名为s的图书。
find(s),表示查询是否存在一本书名为s的图书。
输入
第一行包括一个正整数n(n≤30 000,表示操作数。
以下n行,每行给出2种操作中的某一种指 令条,指令格式为:
add S
find S
在书名s与指令(add,find)之间有一个空格隔开,我们保证所有书名的长度都不超过200。可以假设读入数据是准确无误的。
输出
对于每条find(s)指令,我们必须对应地输出一行yes 或no,表示当前所查询的书是否存在于图书馆内。注意:一开始时图书馆内是没有一本图书的,并且,对于相同字母不同大小写的书名,我们认为它们是不同的。
样例输入
4
add Inside C#
find Effective Java
add Effective Java
find Effective Java
样例输出
no
yes
提示
自己想

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");
		}
	}
}

题目描述
RPK要带MSH去一个更加神秘的地方!
RPK带着MSH穿过广场,在第1618块砖上按下了一个按钮,在一面墙上随即出现了一个把手。RPK握住把手,打开了一扇石质大门。他们穿过悠长而芬芳的小道,走到了一扇象征时间的大门――“the gate of time”。
门上写着一个关于时间的谜题“承诺:____年”,RPK思考了一会,从容地用手指写下1万,这时,门开始发出闪光,MSH感觉到自己的心跳都快停止了。
门开了,眼前是一座美丽的神秘花园!
正当RPK和MSH准备进入的时候,突然出现了一个看门的老大爷QL。
QL:“你们干什么你们,还没买票呢!”
RPK突然想起来现金全拿去买蛋糕了,RPK很绅士的问:“能刷卡么?我身上没现金。”
QL:“没钱?那你们不能进去!”
RPK(汗):“……”
QL:“等等,我这有道不会的数学题,你解了我就让你们进去。”
(众人:“……”)
有一个数列{an},a0=1,ai+1=(A*ai+ai mod B)mod C,要求这个数列第一次出现重复的项的标号。
这点小问题当然难不倒数学bug男RPK了,仅凭心算他就得到了结果。
输入
一行3个数,分别表示A B C
输出
输出第一次出现重复项的位置,如果答案超过2000000 输出-1
样例输入
2 2 9
样例输出
4
提示
自己想
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");
}

题目描述
不同的雪花往往有不同的形状。北方的同学想将雪花收集起来,作为礼物送给在南方的同学。共有 n个时刻,给出每个时刻下落雪花的形状,用不同的整数表示不同的形状。 在收集的过程中,同学们不希望有重复的雪花。你可以从任意a时刻开始,在b时刻停止,在a~b时刻中间的雪花也都将被收集。他们希望收集的雪花尽可能多
输入
第一行一个正整数n。
第二行到第n+1行,表示n个时刻的雪花的形状x。
输出
最多能收集雪花的数量。
样例输入
5
1 2 3 2 1
样例输出
3
提示
n≤1e6,xi≤1e9。
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

猜你喜欢

转载自blog.csdn.net/dhdhdhx/article/details/103192599