字符串hash初步
散列的定义与整数散列
先看一个简单问题:
给出N个正整数,再给出M个正整数,问这M个数中的每个数分别是否在N个数中出现过,其中N,M<=105,且所有正整数均不过105.
对于这个问题最直观的思路是:对每个要查询的正整数x,遍历所有N个数,看是否有一个属与x相等。这种做法的时间复杂度是O(NM),当N与M都很大,显然是无法承受的。
不妨让空间来换时间,设置bool型数组hashTable[100010]来判断这个数是否存在,在输入时就令hashtable[i] = true(hashtable数组要提前初始化为false),在输入M个数时就可以直接做判断 ,复杂度由O(NM)变成了O(N+M)
示例:
#include<bits/stdc++.h>
#include<string>
#include<algorithm>
using namespace std;
#define maxn 100010
bool hashtable[maxn] = {false};
int main(){
int n,m,k;
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>k;
hashtable[k] = true;
}
for(int i=0;i<m;i++){
cin>>k;
if(hashtable[k]==true){
cout<<"YES"<<endl;
}
else{
cout<<"NO"<<endl;
}
}
}
如果要求M个要查询的数每个数在N中出现的次数,那么可以把hashtable换成int型,输入n个数时hashtable[i]++表示这个数出现的次数,复杂度O(N+M)
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int hashtable[N] = {0};
int main(){
int n,m,k;
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>k;
hashtable[k]++;
}
for(int i=0;i<m;i++){
cin>>k;
cout<<k<<" "<<hashtable[k]<<endl;
}
}
上面示例都是空间换时间的经典思想——将输入的数作为下标,对这个数的性质进行统计
如果是数的范围在105内这个方法是可行的,但是如果这个数本身超过了105的范围呢?
这个时候就要就需要**散列(hash)**了。
散列的定义:将元素通过一个函数转换为整数,使得该整数尽可能唯一的表示一个元素,也就是说一个元素变换前为Key,变换后会变成H(Key)
散列函数一般有三种:直接定址法,除留余数法,平方取中法
其中最常用的是除留余数法,即对于Key使得H(Key) = Key%mod,但是这样会有许多数
虽然不相等,但是获得的H(Key)是一样的,我们可以选择一个素数来做mod,这样可以尽可能的让不同的Key得到的H(Key)也不同,但是就算是素数做mod也依然会出现两个不同的Key拥有相同的H(Key)的情况,这个时候会出现冲突,冲突不能避免,但是可以解决
1)线性探查法(Linear Probing)
如果H(Key)被占用,那么就将H(Key)+1的位置用来存放Key,但是这样会出现数据过多导致数据扎堆的情况,如果连续多个位置都被使用的话,那么存储数据会很费时间
2)平方探查法(Quadratic probing)
如果H(Key)的位置被占的时候,就依次查询H(Key)+12,H(Key)-12,H(Key)+22,H(Key)-22…H(Key)+k2,如果k2超出范围的话就将H(Key)+k2取模,如果出现H(Key)-k2<0的情况,就探查((H(Key)-k^2)%mod+mod)%mod,
3)链地址法(拉链法)
和上面两种方法不同,链地址法不计算新的hash值,而是把所有H(Key)相同的key连接成一条单链表。
还有STL中的map也可以直接帮助我们hash,所以以上可以只做了解
字符串hash
字符串hash是指将一个字符串S映射成一个整数,使得该整数尽可能唯一的代表字符串S.
假设字符串由A~Z组成他们可以由0-25表示,这样就可以按照26进制转化10进制的思路来hash字符串S,实现代码如下:
int hashFun(char s[],int len){
int id = 0;
for(int i=0;i<len;i++){
id = id*26 + s[i]-'A';
}
return id;
}
如果字符串由AZ与az组成,那么问题就从26进制转换10进制变成了52进制转换为10进制的问题,实现代码:
int hashFun(char s[],int len){
int id = 0;
for(int i=0;i<len;i++){
if(s[i]>='A'&&s[i]<='Z')//在ASCII码中,A~Z的码为65-90而a开始为97,所以要分开算
id = id*52 + s[i]-'A';
else if(s[i]>='a'&&s[i]<='z'){
id = id*52 + s[i]-'a'+26;
}
}
return id;
}
如果字符串中出现数字的话有两种处理方法:
1)按照小写字母的处理方法,进制数增大至62
2)如果确定数字在字符串末尾,那么可以先算出前几位,最后只要加上最后一位数字即可
int hashFun(char s[],int len){
int id = 0;
for(int i=0;i<len;i++){
if(s[i]>='A'&&s[i]<='Z')
id = id*62 + s[i]-'A';
else if(s[i]>='a'&&s[i]<='z'){
id = id*62 + s[i]-'a'+26;
}
else if(s[i]>='0'&&s[i]<='9'){
id = id*62+s[i]-'0'+52;
}
}
return id;
}
问题:
给出N个字符串刚好都是由三个大写字母表示的,查询M个字符串,问每个字符串的出现次数
#include<bits/stdc++.h>
using namespace std;
const int N = 1000;
int hashTable[26*26*26+10]={0},n,m;
char s[N][5],temp[5];
int hashFun(char s[],int len){
int id = 0;
for(int i=0;i<len;i++){
id = id*52 + s[i]-'A';
}
return id;
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>s[i];
int id = hashFun(s[i],3);
hashTable[id]++;
}
for(int i=0;i<m;i++){
cin>>temp;
int id = hashFun(temp,3);
cout<<hashTable[id]<<endl;
}
}
字符串hash进阶
如果对较长的字符串使用上面的hash方式会出现存储溢出的现象,我们可以对这个数进行取模运算。不过在int数据范围内,如果把进制数设置成一个107级别的素数p(如:10000019),同时把mod换成109的级别的素数(例如:1000000007),那么冲突的概率会变得很小,如下所示:
H[i] = (H[i-1]*p+index(str[i]))%mod
来看问题:
给出N个只有小写字母的字符串,求其中不同的字符串的个数
#include<bits/stdc++.h>
using namespace std;
const int MOD = 1000000007;
const int P = 10000019;
#define N 10000
int a[N];
char s[N][N];
int hashFun(char s[]){
int ls = strlen(s),id = 0;
for(int i=0;i<ls;i++){
id = (id*P+s[i]-'a')%MOD;
}
return id;
}
int main(){
int n,ans = 0;
cin>>n;
for(int i=0;i<n;i++){
cin>>s[i];
a[i] = hashFun(s[i]);
}
sort(a,a+n);
for(int i=0;i<n-1;i++){
if(i==0||a[i]!=a[i+1]){
ans++;
}
}
cout<<ans;
}
感谢阅读!