【问题标题】:Seemingly easy FNV1 hashing implementation results in a lot of collisions看似简单的 FNV1 散列实现会导致很多冲突
【发布时间】:2016-10-01 12:49:47
【问题描述】:

我正在使用哈希表并使用约 350,000 个英语单词的语料库,我想尝试均匀分布。因此,我尝试将它们放入一个长度为 810,049 的数组中(最接近的素数大于输入大小的两倍),我很困惑地看到这样一个简单的 FNV1 实现:

public int getHash(String s, int mod) {
        final BigInteger MOD = new BigInteger(Integer.toString(mod));
        final BigInteger FNV_offset_basis = new BigInteger("14695981039346656037");
        final BigInteger FNV_prime = new BigInteger("1099511628211");

        BigInteger hash = new BigInteger(FNV_offset_basis.toString());

        for (int i = 0; i < s.length(); i++) {
            int charValue = s.charAt(i);

            hash = hash.multiply(FNV_prime).mod(MOD);
            hash = hash.xor(BigInteger.valueOf((int) charValue & 0xffff)).mod(MOD);
        }

        return hash.mod(MOD).intValue();
    }

导致 64,000 次碰撞,很多,基本上是输入的 20%。我的实施有什么问题?这种方法是否存在某种缺陷?

编辑:除此之外,我还尝试并实现了其他散列算法,如 sdbm 和 djb2,它们的性能完全相同,同样糟糕。在这个语料库上都有这些 ~65k 的碰撞。当我将语料库更改为仅表示为字符串的 350,000 个整数时,开始出现一些差异(例如一种算法有 20,000 次碰撞,而另一种算法有 40,000 次),但碰撞次数仍然高得惊人。为什么?

EDIT2:我刚刚对其进行了测试,Java 的内置 .hashCode() 会导致同样多的冲突,即使你做了一些非常幼稚的事情,比如哈希是所有字符模数乘积的乘积810,049,它的性能只比所有那些臭名昭著的算法差一半(60k 碰撞与 90k 与天真的方法)。

【问题讨论】:

  • 你打算让调用者作为mod传递什么?

标签: java hash


【解决方案1】:

由于mod您的 哈希函数的参数,我认为它是您希望哈希标准化的范围,即对于您的特定用例,您期望它是810,049 .我假设这是因为:

  1. 该算法要求进行模 2n 的计算,其中 n 是所需散列中的位数。
  2. 鉴于 offset basisFNV Prime 是模块内的常量,并且等于 64 位哈希的参数,mod 的值也应固定为 264
  3. 既然不是,我假设它是所需的最终输出范围。

换句话说,给定一个固定的偏移基础和 FNV Prime,没有理由传入 mod 参数——它由其他两个 FNV 参数决定。 如果以上所有都正确,那么实施是错误的。您应该进行计算 mod 264 并应用 810,049 的最终余数运算。

此外(但这可能并不重要),该算法要求将低 8 位与 ASCII 字符进行异或,而您使用 16 位进行异或。我不确定这会有所不同,因为对于 ASCII,高位字节无论如何都会为零,并且它的行为就像您只对 8 位进行异或运算一样。

【讨论】:

  • 是的,mod810,049 的预期范围。至于你的其余回复,当然你是对的,FNV 应该使用 2^64 作为模数,但由于我最后要用810,049 对其进行修改,这似乎不是问题, 可以?即使确实如此,但事实证明(我在主帖的编辑中添加),问题几乎不是 FNV 特定的,因为无论使用何种算法,类似(并且同样高)的碰撞次数持续存在。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-30
  • 1970-01-01
  • 1970-01-01
  • 2018-11-02
  • 2020-08-13
  • 1970-01-01
相关资源
最近更新 更多