哈希取余法、哈希表大小取质数的问题

原文地址


哈希取余法、哈希表大小取质数的问题

  1. hashing | planetmath.org http://planetmath.org/node/33326
  2. good hash table primes | planetmath.org http://planetmath.org/goodhashtableprimes
  3. 哈希函数取余法除数为何要取质数? - SegmentFault http://segmentfault.com/q/1010000000593741
  4. 为何哈希函数取余法要避免2的幂? - SegmentFault http://segmentfault.com/q/1010000000593556
  5. 哈希表详解 - 承续缘的信仰 - 博客频道 - CSDN.NET http://blog.csdn.net/liangbopirates/article/details/9753599

  6. 之前一直好奇,为什么哈希表的大小需要取质数,而且还要取最好远离2的幂次方的质数?
    任意一个数x mod m的值,与m的值应该没有什么关系吧。
    看了上面一些讲的,自己总结一下。

    第一个链接planetmath讲的hashing里面,我觉得说的挺对的,哈希函数的实质,就是把一个键值空间为K的映射到hashtable的空间T中。
    hash函数的设置有两个主要的要求:
    1.hash的计算要快,效率要高,即hash的计算公式要比较简单,不要太复杂
    2.hash的冲突率要低
    这里它说了两种hash函数的构造方法,一个是除,一个是乘:h(k)=f(k)(modn);h(k)=⌊n⋅((f(k)⋅r)(mod1))⌋,r是0到1的数。
    虽然都想要那种能够one to one的完美hash函数,不过一般很难办到,所以就有hash冲突,hash冲突就是两个不同的键值k1,k2,
    但是h(k1)=h(k2),这就造成冲突。
    这里我们定义一个负载因子:l=t/h,其中t是代表hashtable中已经被填好的槽,h是hashtable的总槽数。
    很明显,当l接近于1的时候,冲突是不可避免的。
    为了避免冲突的话,有三种解决方案:
    1.把hashtable的大小开的足够大,比键值空间K的范围大得多
    2.采用拉链法
    3.选择一个完美hash函数,是one to one的。
    考虑option 1,设hash表中实际被填满的槽为A,A是K的子集,则有|A|<c|T|,实验效果发现,c取1/2到3/4
    之间比较好。
    考虑option 2,拉链法就没有冲突不冲突了,如果冲突了,直接再申请一块内存空间存下来就好了。
    考虑option 3,所谓的完美one to one的hash函数,其实主要是由目标数据和可用内存空间两个一起决定的
    举个简单例子,h(k)=k肯定是一个完美hash,不过你的内存是不是能够把它们都保存下来就不一定了。
    然后提了两种hash:
    1.接近完美hash的函数,可以把n个映射到n个,没有冲突,这个需要好好构造。
    2.能够直接进行排序的,如果k1<k2,就有h(k1)<h(k2),这样相当于节约了排序的时间消耗o(nlog(n)),当K
    小于T的时候是可行的,不过由于这样肯定会在hash函数中增加额外的计算,这样无形中对hash的效率是有
    一定影响的。

    第二个链接主要是对hash表的大小取质数进行了一个简单的介绍。
    它提供了一个hash表大小取值list,list里面的书都有3个特点:
    1.都是质数
    2.都是前一个质数的2倍小一点点
    3.都是远离2的幂次方的数
    至于为什么这么选的原因
    第一个质数,从第三个和第四个链接里面看的话,个人觉得滕亦飞的答案比较正确。其实如果键值的取值是均匀的,没有什么特点,当K<T的时候,其实不管T的取值是多少,都是最优的,概率都是平均的。不过如果当K>T了,那么就是他分析的那样了,只要是K,T的公因子的那个就会比较吃香,映射过去的就比较多。所以自然是选择质数,概率范围最大了。
    第二个要是2倍小一点点,主要是考虑到数据量的增大,那么,这么选取比较方便
    第三个就纯粹说的是allegedly,这样的实验效果比较好的缘故了。

    第三个链接里面说,单纯取余的哈希运算很糟糕。“远离2^i的质数”、“接近2^i的质数”、“恰好等于2^i”都只不过是“没有那么差”、“确实很差”和“差到不能再差”的区别。
    个人觉得说得不对,还是根据之前的分析,如果说因为电脑是由二进制来代表数字,就说2的幂次方不适合做哈希表的大小,或者说效果很差,原因不是完全不是因为是二进制,2的幂次方,而是因为他是合数,并且数据本身分布不均匀导致的。
    如果数据分布均匀,其实不管是2的幂次方还是3的幂次方都会是一样的结果。
    还有就是第四个链接里面说的,感觉一部分说的挺对的。
    不过里面也说,首先不能选择2的倍数,原因是这样相当于把数字的高位截断了,如果数字恰巧只有高位部分,那就悲剧了。从某种程度上说,感觉说得挺对的,不过和他自己之前的理论就有点儿矛盾。本来他自己就说选啥都OK,结果现在又说选2的幂次方有问题,这个我不能接受。
    我觉得根本性的问题还是在于他本身是一个质数上。
    因为所谓的高位截断,其实就是说如果选择的是m大小的,那么k*m的都会被映射到同一个位中,这是很正常的一件事,因为你不能说计算机选择什么进制就不能以该数的幂次方来作为哈希表大小。
    当然我承认,单纯取余的哈希运算很糟糕,但是作为映射到哈希表中的最后一步填槽运算的时候,其实影响不大了,可能就和链接1,2里面说的那样,只要是个质数就行,只不过实验效果来看,原理2的幂次方的质数效果稍微好点儿。
    其实可能还有一个理由,有些哈希函数对字符串处理的时候,采用的就是ANSI编码或者什么编码方式,这种编码方式的话,可能弄出来的字符串转成整数时,出现2的幂次方的可能性比较大,数据分布很不均匀导致的。

    综上所述:
    1.单纯取余哈希算法效果不好的原因主要是因为数据本身分布不均匀导致的,在数据分布均匀的情况下,不管你取的是质数还是合数,对于最后的结果都没有影响,只要你的数据是平均分布在0-k*m的区间范围之间的。
    2.不过一个好的哈希函数,就是需要在各种不同的数据分布情况下,其效果是最优的。考虑到这一点,哈希取余算法自然最好是一个质数,因为根据链接四滕亦飞里面他推导的公式,可以知道,作为质数,最后求出来的值的值域最大,在0-m-1之间
    3.由2可知,2的幂次方就首先被排除了,不仅是2的幂次方、3的幂次方,4的幂次方……都被排除了。但是,为什么不能取距离2的幂次方的质数、远离2的幂次方的质数,这一点,我认为是没有理论依据的,只能说,根据实验效果来看,远离2的幂次方的质数效果比较好,所以选择这个。
    4.上面3点说的都是作为哈希函数的取余算法的效果,然而作为使用哈希表的,不管你之前做了什么哈希函数的映射,到最后一步,存到哈希表中的时候,都会对哈希表的大小进行一个取余的运算,使数据尽量均匀分布在槽里面。由上面说的,如果你之前的哈希函数足够好,把数据量都均匀分布了,那选啥都没问题;但为了防止之前的哈希不够好,自然哈希表的大小开具为一个质数比较OK,而且根据实验效果来看,开具成一个尽量远离2的幂次方的哈希表大小比较OK。

如果有说错的地方欢迎大家批评指正!

posted @ 2019-08-08 13:35  寒菱  阅读(1930)  评论(0编辑  收藏  举报