【问题标题】:Simple general-purpose hash function for a collection集合的简单通用哈希函数
【发布时间】:2013-08-14 11:14:19
【问题描述】:

请标记为重复,但到目前为止我发现的大多数问题都过于具体或比我正在寻找的更复杂。例如。在"What is a good hash function" 中,接受的答案似乎是面向散列字符串的。

我最近开始使用 .NET 进行编程,但遗憾的是,内置类无法执行一些基本操作,例如检查相等性和查找其哈希值。我相信他们有他们的设计原因。无需为 .NET 辩护。当我需要使用集合作为字典的键时,我只想知道如何避免显着的偏题。例如,我希望两个包含所有相等值的不同 List 对象映射到字典中的同一条目。开箱即用,它们不这样做:List 的默认行为是 List 不等于除自身之外的任何东西,因此具有相同值的列表的另一个实例是不同的键。

实现 Equals 很简单。这是我不确定的哈希函数。

在我的 GetHashCode 实现中是否提供了一些可以调用的东西?

如果我必须从头开始编写,那么真正简单但足够好的哈希算法是什么?我可以使用 SHA1,但我认为这太过分了。我可以对项目的所有哈希值进行异或运算,但我认为这会有一些令人讨厌的碰撞属性。我不在乎计算哈希是否非常快,但我不希望我的哈希表在具有某些特定分布的数据集上变慢到线性。我想要的是简单到我能记住的东西。如果你能解释(或链接到)它为什么起作用,那么奖励。

【问题讨论】:

    标签: algorithm collections hash


    【解决方案1】:

    在这里要非常小心。如果您为List<T>(或类似集合)创建GetHashCode 方法,那么它可能会执行以下操作:

    public override int GetHashCode()
    {
        int hash = 13;
        foreach (var t in this)
        {
            // X is an operation (undefined here) that somehow combines
            // the previous hash value and the item's hash value
            hash = hash X t.GetHashCode();
        }
        return hash;
    }
    

    (我建议使用Jenkins hash 之类的方法来计算哈希码。还可以查看Wang hash(或位混合器)。)

    除非您第一次计算该值并将其缓存,否则每次调用 GetHashCode 时,您最终都会遍历所有项目。

    因此,您已经为您的集合创建了 GetHashCodeEquals,并将实例放入 Dictionary。现在您必须非常小心,不要更改集合(即不要添加或删除任何项目)或集合内的任何项目。否则GetHashCode 的值将会改变,字典将不再起作用。

    我强烈建议,如果您想使用集合作为字典的键,请确保该集合是不可变的。

    另一件需要考虑的事情。列表相等的概念并不像您所说的那么简单。例如,列表[1, 2, 3, 4, 5][5, 1, 3, 4, 2] 是否相等?这取决于您对平等的定义。当然A.Union(B) == A.Intersect(B),这意味着如果您对相等的定义是“包含相同的项目”,它们是相等的。但如果顺序很重要,那么列表就不一样了。

    如果您的定义是“包含相同的项目”,那么我上面展示的哈希码计算将不起作用,因为哈希码计算取决于顺序。所以如果你想计算这些列表的哈希码,你必须先对它们进行排序。

    如果列表不能包含重复项,那么计算相等性就是创建一个列表的哈希集并从该哈希集中的另一个列表中查找每个项目。如果列表可以包含重复项,那么您要么必须对它们进行排序以确定相等性,要么使用某种带有计数的字典。这两者都意味着列表中包含的对象将实现某种形式的相等比较器等。

    并且一些相等的定义根本不考虑重复。也就是说,[1, 2, 3] 将等于 [3, 3, 3, 2, 1, 1]

    考虑到平等的不同差异以及在定义List<T> 的行为时允许这些差异以及更多的努力,我可以理解为什么设计集合类的人没有实现值平等。特别是考虑到在字典或哈希表中使用 List<T> 或类似集合作为键是非常罕见的。

    【讨论】:

    • 我不知道为什么有人不赞成这个答案,但我赞成 a) 如果要将列表用作集合中的键,它必须是不可变的, b) 集合的哈希码应该被缓存以避免重新计算成本(如果集合是不可变的,这同样更容易)。如果可以的话,我会再次投票赞成最近添加的关于在不同情况下两个列表相等可能意味着什么的讨论,这是另一个需要牢记的重要点。
    • 很多我已经知道的不请自来的信息,关于实际哈希算法的内容不多。
    • @morningstar:Jenkins 很简单,而且比“足够好”要好得多。您显然已经知道这些不请自来的信息在您的问题中并不明显,而且我倾向于在信息过多方面犯错。
    • Jenkins 仍然面向 8 位数据。至少从 Wikipedia 链接我推断它平等地对待所有 8 位。可以将每个项目的 GetHashCode 结果视为 4 个 8 位值的序列。我想这会奏效。我会为链接给出最佳答案。
    【解决方案2】:

    根据我的经验,如果您有一组事物并且想要计算它们的哈希值,最好分别计算每个单独对象的哈希值;将所有这些哈希值收集到一个数组中。最后,计算散列值数组的散列值。

    所有较简单的技术都相对较快地失效。 (比如将这些值进行异或运算或乘以幻数并求和——这些都有各种各样的病态失败案例。)最后计算的一个额外的数组散列是一个很小的成本,并且总体上得到了回报。

    【讨论】:

    • “计算散列值数组的散列” - 说明这是一个显而易见的步骤。
    • 在我的例子中,我使用 Bob Jenkins 的“lookup3”算法来散列一块内存。 burtleburtle.net/bob/hash/doobs.html 你可以使用任何你喜欢的方法——CRC32、MD5、Adler-32,任何可以输入不透明内存块并返回哈希值的方法。
    【解决方案3】:

    一个好的散列函数同样适用于任何位的字符串——不仅仅是字符。但是,由于集合可能:

    1. 不一定位于连续的内存块中,并且
    2. 包含您不想包含在散列中的部分(例如,从链表的一个元素指向另一个元素的指针,这对于具有相同内容的不同链表会有所不同,但在这种情况下,您会希望具有相同的哈希值)。

    ...在我看来,这里的关键问题可能是“将一组单独的哈希值组合起来为集合生成哈希值的最佳方法是什么?”。

    在我看来,对集合中各个元素的哈希值进行异或是一种合理的方法。我可以立即看到的唯一问题是它会导致两个集合具有相同的元素,但包含在不同的顺序中,散列到相同的值。避免此问题的算法可能如下所示:

    1. 查找集合中项目的哈希值。
    2. 通过按照元素在集合中出现的顺序连接这些哈希值来创建位串。
    3. 使用任何合理的散列算法为该位串的散列值生成散列值。
    4. 使用上一步计算的哈希值作为集合的哈希值。

    【讨论】:

    • 对这些值进行异或运算在一般情况下效果不佳。我一直在走这条路,经常看到它失败。您的第二种方法(将哈希结果附加到一个位串中,然后对位串进行哈希处理)效果很好。这基本上是对我在回答中推荐的内容的改写:)
    • 我发现了一个较早的问答集,其中讨论了组合哈希值的方法。见Why is XOR the default way to combine hashes?。简而言之:异或是好的,因为它保持熵,但是异或相同的值会产生零结果(并且相同的值很常见,所以这很糟糕)并且异或是可交换的(这是我在回答中提出的一点),这也可能是不可取的.因此,正如@StilesCrisis 所建议的那样,对单个散列值的位串进行散列是更可取的。
    • 听起来不错,但我遇到过一些字符串散列函数针对 ASCII 字符进行了优化的参考资料。例如。最低 4 位应该是最重要的。
    • 从个人经验来看,我已经看到 XORing 的工作速度非常快,即使它引起的冲突比基准多一点。冲突是不可避免的,任何哈希函数都必然会导致它们。我建议,制作一个小型测试套件。选择多个散列函数并自行比较。我这样做了,速度与碰撞的权衡让我选择了异或。
    • Kshitij,我做了和你一样的选择,然后几年后我想知道为什么我的哈希表会发生如此多的冲突。改变了它,突然间我的哈希表再次运行良好,代价是再次在一小块不透明数据上调用哈希函数。
    猜你喜欢
    • 2013-01-02
    • 2011-06-12
    • 2010-12-11
    • 2018-12-14
    • 2020-08-08
    • 1970-01-01
    • 2011-09-01
    • 2019-10-22
    • 1970-01-01
    相关资源
    最近更新 更多