学习笔记:字符串-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冲突率更低,可以:

  1. 使用单独的一种hash,但通过改变乘数或者余数得到多组hash值来同时进行比较

  2. 使用多种不同的hash,得到多组hash值同时比较

  3. 使用hash表,将同一hash值的冲突的数据存在一个链表里,存在冲突时通过访问列表里的所有元素来确定

    ……

反正方法有很多

乘法Hash(进制Hash)

最基本也是花样最多的一种哈希。(好像还叫BKDRHash)

核心思想就是把字符串看成是一个26进制的数组(这个是对于纯小写\纯大写的字符串,如果加上数字就是36进制,如果区分大小写就是52进制……)然后把他换算回十进制。

如果不能确定取多少作为乘数的话,那就取33就行(好像如果进制数大于33,乘数取33也是不错的)。取31的原因主要有两点:

  1. ​ 33是一个奇质数(虽然偶质数就那一个),它可以保证因数最少,从而尽可能减少哈希冲突的发生;

  2. ​ 33在进行乘法运算时会更快,因为 \(x*33\)​​ 可以被优化成 \((x<<5)+x\)​​

如果得到的十进制数超出了 int 或者 long long 的范围,有下面几种方式来处理:

  1. 使用unsigned让它随便溢出,反正溢出了还是正数;

  2. ​ 取模:

    ​ 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值:

\[hash=hash\_val[r]-hash\_val[l-1]*base^{r-l+1} \]

posted @ 2021-08-13 16:39  RagnaLP  阅读(183)  评论(0编辑  收藏  举报