[关闭]
@AlphaYuan 2018-01-31T14:41:21.000000Z 字数 3459 阅读 475

散列函数

Hash

1. 除法散列法

hash(key)=key mod m,其中key表示被散列的关键字,而m则表示散列表的大小,mod则为取余操作。

这是一种比较简单的散列函数,但简单并不意味着高效。当待散列的元素之间存在某种模式时,这种散列法会有相当糟糕的性能表现。对该函数一个有用的指导原则是将m选取为接近待散列集合大小的质数。

2. 乘法散列法

hash(key)=floor(m×(A×key mod 1)),其中floor()表示对表达式进行下取整,A∈(0,1),m如上同样表示散列表的大小,且在这种方法中对m并无任何特殊的要求。
[A×key mod 1]表示将key乘上某个在0~1之间的数并取乘积的小数部分,该表达式等价于A×key-floor(A×key)

这里最重要的是A的值应该如何设定,Don•Knuth老大认为A=(√5-1)/2 [黄金分割点] 比较好。

3. 全域散列法

hasha,b(key)=(a×key+b) mod m,如同除余散列法中一样,m的值应为质数,而a∈{1,2,3,…,m-1},b∈{0,1,2,…,m-1}且a,b的值应在运行时动态确定。

全域散列的基本思想是给出hash函数的基本“骨架”,而其中的某些参数通过运行时在指定范围内随机选取确定,从而实际上形成了一个函数簇。根据上述a,b的取值范围,我们知道这个函数簇中存在m×(m-1)个函数。因为对于同一个输入,每次执行时选取不同的参数将拥有不同的性能表现,因此设计的函数实际上独立于任意被散列的关键字。只有当一个相对糟糕的输入——即该输入不构成随机分布,遇到一个选取的相对糟糕的散列函数时,才将导致较差的性能,在多数情况下这类散列往往具有较好的运行时性能表现。

4. 平方散列法

求hash(key)是非常频繁的操作,而乘法的运算要比除法更省时,所以考虑把除法换成乘法和一个位移操作,公式为:

hash(key)= (key * key) >> 28 (右移28位,除以2^28。)

如果数值分配比较均匀的话这种方法能得到不错的结果。

5. 斐波那契(Fibonacci)散列法

为了解决平方散列法的缺点,我们需要找出一个理想的乘数,而不是拿key本身当作乘数。
1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485
这几个“理想乘数”的得出跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377, 610,987, 1597, 2584, 4181, 6765, 10946,...

常见的32位整数,使用公式:

Hash(key)= (key * 2654435769) >> 28

6. 加法Hash

所谓的加法Hash就是把输入元素一个一个的加起来构成最后的结果。标准的加法Hash的构造如下:

  1. static int additiveHash(String key, int prime)
  2. {
  3. int hash, i;
  4. for (hash = key.length(), i = 0; i < key.length(); i++)
  5. hash += key.charAt(i);
  6. return (hash % prime);
  7. }

这里的prime是任意的质数,看得出,结果的值域为[0,prime-1]。

7. 位运算Hash

这类型Hash函数通过利用各种位运算(常见的是移位和异或)来充分的混合输入元素。比如,标准的旋转Hash的构造如下:

  1. static int rotatingHash(String key, int prime)
  2. {
  3. int hash, i;
  4. for (hash=key.length(), i=0; i<key.length(); ++i)
  5. hash = (hash<<4)^(hash>>28)^key.charAt(i);
  6. return (hash % prime);
  7. }

先移位,然后再进行各种位运算是这种类型Hash函数的主要特点。比如,以上的那段计算hash的代码还可以有如下几种变形:

  1. hash = (hash<<5)^(hash>>27)^key.charAt(i);
  2. hash += key.charAt(i);
    hash += (hash << 10);
    hash ^= (hash >> 6);
  3. if((i&1) == 0)
    {
    hash ^= (hash<<7) ^ key.charAt(i) ^ (hash>>3);
    }
    else
    {
    hash ^= ~((hash<<11) ^ key.charAt(i) ^ (hash >>5));
    }
  4. hash += (hash<<5) + key.charAt(i);
  5. hash = key.charAt(i) + (hash<<6) + (hash>>16) – hash;
  6. hash ^= ((hash<<5) + key.charAt(i) + (hash>>2));

8. 乘法Hash

这种类型的Hash函数利用了乘法的不相关性(乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算法,虽然这种算法效果并不好)。比如,

  1. static int bernstein(String key)
  2. {
  3. int hash = 0;
  4. int i;
  5. for (i=0; i<key.length(); ++i) hash = 33*hash + key.charAt(i);
  6. return hash;
  7. }

jdk5.0里面的String类的hashCode()方法也使用乘法Hash。不过,它使用的乘数是31。推荐的乘数还有:131, 1313, 13131, 131313等等。
使用这种方式的著名Hash函数还有:

  1. // 32位FNV算法
  2. int M_SHIFT = 0;
  3. public int FNVHash(byte[] data)
  4. {
  5. int hash = (int)2166136261L;
  6. for(byte b : data)
  7. hash = (hash * 16777619) ^ b;
  8. if (M_SHIFT == 0)
  9. return hash;
  10. return (hash ^ (hash >> M_SHIFT)) & M_MASK;
  11. }

以及改进的FNV算法:

  1. public static int FNVHash1(String data)
  2. {
  3. final int p = 16777619;
  4. int hash = (int)2166136261L;
  5. for(int i=0;i<data.length();i++)
  6. hash = (hash ^ data.charAt(i)) * p;
  7. hash += hash << 13;
  8. hash ^= hash >> 7;
  9. hash += hash << 3;
  10. hash ^= hash >> 17;
  11. hash += hash << 5;
  12. return hash;
  13. }

除了乘以一个固定的数,常见的还有乘以一个不断改变的数,比如:

  1. static int RSHash(String str)
  2. {
  3. int b = 378551;
  4. int a = 63689;
  5. int hash = 0;
  6. for(int i = 0; i < str.length(); i++)
  7. {
  8. hash = hash * a + str.charAt(i);
  9. a = a * b;
  10. }
  11. return (hash & 0x7FFFFFFF);
  12. }

虽然Adler32算法的应用没有CRC32广泛,不过,它可能是乘法Hash里面最有名的一个。关于它的介绍,大家可以去看RFC 1950规范。

9. 除法Hash

除法和乘法一样,同样具有表面上看起来的不相关性。不过,因为除法太慢,这种方式几乎找不到真正的应用。需要注意的是,我们在前面看到的hash的 结果除以一个prime的目的只是为了保证结果的范围。如果你不需要它限制一个范围的话,可以使用如下的代码替代”hash%prime”:

hash = hash ^ (hash>>10) ^ (hash>>20)。

10. 查表Hash

查表Hash最有名的例子莫过于CRC系列算法。虽然CRC系列算法本身并不是查表,但是,查表是它的一种最快的实现方式。查表Hash中有名的例子有:Universal Hashing和Zobrist Hashing。他们的表格都是随机生成的。

11. 混合Hash

混合Hash算法利用了以上各种方式。各种常见的Hash算法,比如MD5、Tiger都属于这个范围。它们一般很少在面向查找的Hash函数里面使用。

12. 数组hash

  1. inline int hashcode(const int *v)
  2. {
  3. int s = 0;
  4. for(int i=0; i<k; i++)
  5. s=((s<<2)+(v[i]>>4))^(v[i]<<10);
  6. s = s % M;
  7. s = s < 0 ? s + M : s;
  8. return s;
  9. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注