我们拿除法hash做说明,按照hash的原理很容易知道其他类别的hash都是和除法hash同构的。

假设hash函数为h(k)=k mod m,即m个slot。


m的取值有几个需要注意的地方:


1.m不能取一个2^p的数。算法导论是这样解释的:

这是因为对一个数除以2^p取余数相当于只取这个数的最低的p位,高于p位的信息就被丢弃了。

这个原理很容易理解:

假设把15映射到8个槽位的hash表里,即k=15 m=8。k的二进制表示1111,8的二进制表示1000.取余数为7,二进制表示是111,即k的最低3位。这样相当于不管k的除去最后3位取什么值,结果都是不变的。


2.m不能取一个接近2^p或10^p的数。

算法导论里面举了一个例子:假设m=2^p-1,如果k是一个用p个bit编码的字符,那么这些字符无论怎么组合,hash后得到的结果都是一样的。

这个理解起来稍微有点困难:

先举个例子:

假设我们的p取4,也就是m=2^4-1=15。

我们的想要把4个bit编码的字符所组成的字符串放到hash表中。

这样每个字符串都可以用一个16进制的数来表示。

打开python,随便找一组数计算一下:

0x1234%15=10

0x4231%15=10

0x3214%15=10

....

不管怎样组合,hash得到的结果都是一样的。

这是为什么?


证明一下:

每个字符串的都是p个bit编码的,我们假设2^p=a因此n个字符的字符串的多项式表示就是

X0+aX1+(a^2)X2+(a^3)X3+(a^(n-1))Xn

对于同样的字符所组成的字符串,假设2^(ap)这个系数不变,相当于把许多个Xi和Xj做交换。

因为置换可以表示成有限个对换的乘积。因此,任意两个字符串可以通过有限次两两对换得到。

身为一个搞计算机的对这个原理并不陌生。我们通常用的各种基于比较的排序算法都是这个原理的具体体现。也就是说任何序列都可以通过两个元素交换的方法变成标准序列。同时我们也知道,交换的次数的上界是nlogn

对于任意一次对换,假设我们要对换的(a^i)Xi和(a^j)Xj,对换之前表达式是S0=(a^i)Xi+(a^j)Xj+Sn

Sn表示表达式的剩余部分。

对换之后的表达式S1=(a^j)Xi+(a^i)Xj+Sn。

对换所产生的差值是S1-S0=(a^j)Xi+(a^i)Xj-(a^i)Xi-(a^j)Xj=(a^j-a^j)(Xi-Xj)

我们观察a^j-a^i这个表达式,可以发现a=1是a^j-a^i=0的恒等解。不管i和j取多少,等式都成立。这意味着a-1是a^j-a^i因式分解后的一个因子。

因此说明,对每一次对换,变化量是a-1的整数倍。也就是a^p-1的整数倍。因此对于任意有限次对换相乘形成的变换,变化量始终是a^p-1的整数倍。

这就证明了不管这个字符串采用什么样的组合,他对a^p-1取余数的结果都是不变的。


虽然2^p-1的例子证明了,但是我还没有想到其他接近2^p的数所导致的不良后果,但是宁可信其有吧。


3.m的值为什么要取一个素数?

这个算法导论中没有给出解释,实际上如果学过群论的话,这个问题是非常好理解的。

因为对于一个阶为n的群,他的子群的阶一定是n的因子。(lagrange定理)

所以如果n是一个素数,那么这个群就没有真子群。

怎么理解这个原理:

我们做hash table的所需要避免的现象是什么?

就是我们实际上要放的元素并没有在hash table中均匀分布,只是占用了一部分,大量的空间被浪费了。因为hash空间是有限的,想要刻意找到无限多个碰撞也是能够办到的。我们假设用户并不是刻意去捣乱,而是碰巧就产生了这个现象。

这种巧合是怎么形成的?

因为想往hash表中放的元素本身也是有规律性,这个规律与hash表本身的设计相叠加,产生了这样的后果。

如果我们m不是一个素数的话,m的每一个因子都可以构造一种循环导致问题产生。

还是举个例子:

假设m=6时候,我们的槽位相当于<0,1,2,3,4,5>

因为6=2*3

那么<0,2,4>,<0,3>都代表了产生问题的情况。

假设用户的放的k的值满足每个等差加2,或者每个等差加3,都会导致只占用hash table中的一部分,而不是全部。

为了避免这种情况出现,m需要取一个素数。