学习笔记:字符串-Hash
Hash
Hash算法(哈希算法)实际上就是将一串数据(一般是数组或字符串)通过一些特定的方法转化成可以代表这些数据的一个数(Hash值)。通过哈希就可以快速的完成对这一串数据的一些比较,比如说当你要检验很多组字符串之间有哪些是一样的,就可以先算出各个字符串的Hash值,再通过比较Hash值是不是一样来替代更慢的普通字符串比较。
或者说Hash是一种从大范围到小范围的映射,数组或者字符串是一组大范围的数据,而通过Hash处理后得到的Hash值就是一个小范围的数据(一般是一个整型)。
从再数学一点的角度来看,Hash就是一个数学函数,你给它一些数据,它给你一个特征值。你给它的数据只能转化成一个特征值,同时理想状态下一个特征值只对应一组数据。
要想做到让这个Hash值能够代表这一串数据,就需要使用乘法或者位运算(或者全都要)。
Hash冲突
由于Hash实际上是通过一些运算来计算出Hash值,所以有可能会出现明明是两个不同的字符串 $a , b $ ,但是最后却得到了一样的Hash值( \(hash(a)==hash(b)\) ),这种情况我们就叫做Hash冲突。
当然并不是Hash所有都一定会有冲突(康托展开就是一个很好的例子),但是在面对由于数据太多的而不能保证无冲突的时候,我们要做的就是选择一个最好的Hash方式来尽可能的减小Hash冲突的发生率来保证运行结果的正确性。
Hash种类
Hash有超级多种,如果想要Hash冲突率更低,可以:
-
使用单独的一种hash,但通过改变乘数或者余数得到多组hash值来同时进行比较
-
使用多种不同的hash,得到多组hash值同时比较
-
使用hash表,将同一hash值的冲突的数据存在一个链表里,存在冲突时通过访问列表里的所有元素来确定
……
反正方法有很多
乘法Hash(进制Hash)
最基本也是花样最多的一种哈希。(好像还叫BKDRHash)
核心思想就是把字符串看成是一个26进制的数组(这个是对于纯小写\纯大写的字符串,如果加上数字就是36进制,如果区分大小写就是52进制……)然后把他换算回十进制。
如果不能确定取多少作为乘数的话,那就取33就行(好像如果进制数大于33,乘数取33也是不错的)。取31的原因主要有两点:
-
33是一个奇质数(
虽然偶质数就那一个),它可以保证因数最少,从而尽可能减少哈希冲突的发生; -
33在进行乘法运算时会更快,因为 \(x*33\) 可以被优化成 \((x<<5)+x\)
如果得到的十进制数超出了 int 或者 long long 的范围,有下面几种方式来处理:
-
使用unsigned让它随便溢出,反正溢出了还是正数;
-
取模:
hash里关于取模的模数(哈希因子)该怎么取是一个非常经验的东西,这里有一个常用哈希模数表,或者直接记两个:int 范围内 :\(402653189\) ; long long 范围内:\(212370440130137957\) (其实直接拿 \(1e7+7\) 和 \(1e9+9\) 也是可以的)
代码的话就是这样:
ull hash(string x){
ull res=0;
int hash_base=33;
//int hash_mod=402653189;
for(int i=0;i<x.length();i++){
res=res*hash_base+x[i];
//res%=hash_mod;
}
return res;
}
位运算Hash
位运算的hash快到起飞 而且也很好记
它主要是通过异或和移位来让每一个数据都能影响到最后的hash值。相当于是让hash值的不同几位保存几个数据异或的结果。
代码的话是这样:
ull hash(string x){
ull res=0;
for(int i=0;i<x.length();i++){
res=(res<<4)^(res>>28)^x[i];
}
return res;
}
FNVHash
乘法Hash的一种高级变种玩意。全称叫 Fowler-Noll-Vo算法
它同时使用位运算和乘法来计算hash值。这玩意就是硬记一下hash初始值和乘数(这个是固定的对应值,不要乱改):
hash值位数 | hash初始值 | 乘数 |
---|---|---|
32 位 | 2166136261 | 16777619 |
64 位 | 14695981039346656037 | 1099511628211 |
代码的话就是这样:
ull hash(string x){
ull res=2166136261;
int FNV_prime=16777619;
for(int i=0;i<x.length();i++){
hash^=x[i];
hash*=FNV_prime;
}
return hash;
}
其实上面这个是FNVHash的一种,叫FNV-1a ,还有就是交换了一下异或和乘的顺序的FNV-1 (他们说FNV-1a是要比FNV-1好一点,尽量用FNV-1a)
Hash的应用
其实只要扯到字符串判断啊、数组判断啊、枚举字符串减少重复枚举啊都可以用hash(想用就用就行)
子串判断
(其实这玩意应该说是乘法hash的应用)
根据乘法hash的性质,我们可以得到这样一个递推求hash的方法:( 其实就是拿数组存了普通乘法hash里每一个的res值,相当于是当前字符串的hash值)
ull hash_val[1000010]={0};
ull hash(string x){
int hash_base=33;
for(int i=0;i<x.length();i++){
hash=res*base+x[i];
}
return hash;
}
如果我们现在有一个字符串 \(x\) ,我们现在想要求 \(x[l]\sim x[r]\) 这个区间的子串的值,我们只需要知道 \(hash\_val[l]\) 和 \(hash\_val[r]\) 就可以计算出这个子串的hash值: