zoukankan      html  css  js  c++  java
  • 学习笔记:字符串-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} ]

  • 相关阅读:
    28完全背包+扩展欧几里得(包子凑数)
    HDU 3527 SPY
    POJ 3615 Cow Hurdles
    POJ 3620 Avoid The Lakes
    POJ 3036 Honeycomb Walk
    HDU 2352 Verdis Quo
    HDU 2368 Alfredo's Pizza Restaurant
    HDU 2700 Parity
    HDU 3763 CDs
    POJ 3279 Fliptile
  • 原文地址:https://www.cnblogs.com/lazy-people/p/15138030.html
Copyright © 2011-2022 走看看