快速简单的哈希码组合

Posted

技术标签:

【中文标题】快速简单的哈希码组合【英文标题】:Quick and Simple Hash Code Combinations 【发布时间】:2010-12-11 10:25:24 【问题描述】:

人们能否推荐快速简单的方法来组合两个对象的哈希码。我不太担心冲突,因为我有一个可以有效处理冲突的哈希表,我只想要尽快生成代码的东西。

阅读 SO 和网络似乎有几个主要的候选者:

    异或 使用素数乘法进行异或运算 简单的数字运算,如乘法/除法(带有溢出检查或环绕) 构建字符串,然后使用字符串类哈希码方法

人们会推荐什么以及为什么?

【问题讨论】:

【参考方案1】:

我个人会避免异或 - 这意味着任何两个相等的值都会导致 0 - 所以 hash(1, 1) == hash(2, 2) == hash(3, 3) 等等。还有 hash(5 , 0) == hash(0, 5) 等可能偶尔出现。我特意将它用于集合散列 - 如果你想散列一系列项目并且你关心排序,那很好。

我通常使用:

unchecked

    int hash = 17;
    hash = hash * 31 + firstField.GetHashCode();
    hash = hash * 31 + secondField.GetHashCode();
    return hash;

这就是 Josh Bloch 在 Effective Java 中建议的形式。上次我回答了一个类似的问题时,我设法找到了一篇对此进行了详细讨论的文章 - IIRC,没有人真正知道它为什么运作良好,但确实如此。它也易于记忆、易于实施,并且易于扩展到任意数量的字段。

【讨论】:

看起来像 Dan Bernstein(或 Chris Torek)的散列,只是常数不同。也没有人知道为什么这会很好。 一句警告,这是 Berstein 哈希的(一种变体),并且由于没有人知道它为什么在测试中表现出色,因此不建议在哈希至关重要时使用。见eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx。此外,您应该将此代码包装在 unchecked 块中。 GetHashCode() 不应抛出任何异常。 @tofutim:31 是一个不错的选择,因为乘以 31 可以优化为移位和减法。是否以这种方式优化取决于平台。至于为什么这些数字很适合散列 - 正如 Henk 所说,这有点神秘。 @rory.ap:我认为这是一部出色的作品,我非常乐意使用这些数字。虽然我不愿意承认“因为别人说过”而使用常量,但这基本上就是 17/31 对的含义。 从 .NET Core 2.1 开始,您可以使用 System.HashCode 类型的 Combine 方法来做到这一点docs.microsoft.com/en-us/dotnet/api/system.hashcode.combine【参考方案2】:

如果您使用 .NET Core 2.1 或更高版本或 .NET Framework 4.6.1 或更高版本,请考虑使用 System.HashCode 结构来帮助生成复合哈希代码。它有两种操作模式:添加和组合。

使用Combine 的示例,通常更简单,最多可用于八个项目:

public override int GetHashCode()

    return HashCode.Combine(object1, object2);

Add使用示例:

public override int GetHashCode()

    var hash = new HashCode();
    hash.Add(this.object1);
    hash.Add(this.object2);
    return hash.ToHashCode();

优点:

.NET 本身的一部分,从 .NET Core 2.1/.NET Standard 2.1 开始(不过,请参阅下面的内容) 对于 .NET Framework 4.6.1 及更高版本,Microsoft.Bcl.HashCode NuGet 包可用于向后移植此类型。 看起来有很好的性能和混合特性,基于作者和审稿人之前所做的工作merging this into the corefx repo 自动处理空值 采用IEqualityComparer 实例的重载

缺点:

在 .NET 4.6.1 之前的 .NET Framework 上不可用。 HashCode 是 .NET Standard 2.1 的一部分。截至 2019 年 9 月,.NET 团队拥有no plans to support .NET Standard 2.1 on the .NET Framework,即.NET Core/.NET 5 is the future of .NET。 通用,因此它不能处理超级特定的情况以及手工编写的代码

【讨论】:

您可以参考nuget.org/packages/Microsoft.Bcl.HashCode 使其正式在.NET Framework 4.6.1 或.NET Standard 2.0 上运行。 System.HashCode 使用 xxHash32 (github.com/Cyan4973/xxHash)【参考方案3】:

虽然 Jon Skeet 的答案中概述的模板通常作为散列函数系列效果很好,但常数的选择很重要,答案中提到的 17 的种子和 31 的因子效果不佳完全适用于常见用例。在大多数用例中,散列值比int.MaxValue 更接近于零,并且联合散列的项目数为几十个或更少。

对于一个整数元组x, y(其中-1000 <= x <= 1000-1000 <= y <= 1000)进行散列处理,它的碰撞率几乎是98.5%。例如,1, 0 -> 0, 311, 1 -> 0, 32 等。如果我们将覆盖范围扩大到还包括 n 元组,其中3 <= n <= 25,碰撞率大约为 38%,它不会那么糟糕。但我们可以做得更好。

public static int CustomHash(int seed, int factor, params int[] vals)

    int hash = seed;
    foreach (int i in vals)
    
        hash = (hash * factor) + i;
    
    return hash;

我编写了一个蒙特卡罗抽样搜索循环,该循环使用各种种子值和因子对随机整数 i 的各种随机 n 元组进行了测试。允许的范围是2 <= n <= 25(其中n 是随机的,但偏向范围的下限)和-1000 <= i <= 1000。每个种子和因子对至少执行了 1200 万次独特的碰撞测试。

运行大约 7 小时后,发现的最佳配对(种子和因子都限制在 4 位或更少)是:seed = 1009factor = 9176,碰撞率为 0.1131%。在 5 位和 6 位领域,存在更好的选择。但为了简洁起见,我选择了前 4 位数的表现,它在所有常见的intchar 散列场景中都表现得很好。它似乎也适用于更大数量的整数。

值得注意的是,“成为主要成员”似乎并不是作为种子和/或因素获得良好表现的一般先决条件,尽管它可能会有所帮助。上面提到的1009 实际上是素数,但9176 不是。我明确测试了这方面的变化,我将factor 更改为9176 附近的各种素数(同时离开seed = 1009),它们的性能都比上述解决方案差。

最后,我还比较了通用 ReSharper 推荐函数系列 hash = (hash * factor) ^ i; 和上面提到的原始 CustomHash() 严重优于它。对于常见用例假设,ReSharper XOR 样式的冲突率似乎在 20-30% 范围内,我认为不应该使用。

【讨论】:

哇。我喜欢这个答案的工作。令人印象深刻,干得好! 似乎是最好的,但有两条评论:第一个也是简单的,为什么不将“种子”和“因子”移到最后并给它们一个默认值(1009 和 9176)应该为大多数人做这项工作。第二点:就像 Jon Skeet 算法一样,它依赖于顺序,如果你以不同的顺序喂食,你可以获得不同的哈希值。我想知道如果您以不同的方式提供算法,首先对该数组进行排序以确保最后具有相同的最终哈希是否更安全。这样会更安全。 @EricOuellet 因为params int[] vals 必须出现在所有函数参数的末尾,所以我无法设置seedfactor 默认参数。如果您不关心 params 语法的便利性,您可以删除它,然后按照您的建议重新排列参数以允许使用默认值。 @EricOuellet 数组的默认散列应该考虑排列(这是更一般的情况),因此不同排序的散列会有所不同(就像字符串 "abc" 的散列是不同于“acb”的哈希值)。如果您特别想要一个仅用于组合的哈希函数,您可能应该接受 HashSet<int> 参数(假设没有重复)。否则,您可以将函数重命名为 CustomHashCombination() 以防止混淆,并按照建议进行内部预排序。 我喜欢这个答案,但我不会使用params,因为它必须在每次调用时分配一个数组。所以它在计算方面可能更快,但它会为以后产生 GC 压力。【参考方案4】:

在元组中使用组合逻辑。该示例使用 c#7 元组。

(field1, field2).GetHashCode();

【讨论】:

好主意,但我怀疑这可能与 GC 流失有关,因为您隐式创建了一个短暂的对象 @RobV 元组是值类型,因此它们是堆栈分配的,不会施加 GC 压力。 一个问题... (0,1,2).GetHashCode() 和 (0,0,1,2).GetHashCode() 都产生相同的值:35。而在投票最多的答案产生唯一值 0、1、2:506480 和 0、0、1、2:15699890 哈希码不能保证是唯一的。你发现了一个不是这样的情况......除非有很多冲突,否则它不会成为一个糟糕的选择(在这种情况下,提交一个错误是个好主意)。我个人更喜欢使用框架中的一些东西,而不是实现一些不同的东西。 它实际上是结构体的ValueTuple 类型(MSDN)。注意Tuple 类型是一个类,它有 GC 压力。我喜欢这种方式。在内部,它类似于@Stipo 的帖子,但很容易理解和审查。在大多数情况下,这将是一个不错的选择。【参考方案5】:

我认为 .NET Framework 团队在测试他们的 System.String.GetHashCode() 实现方面做得不错,所以我会使用它:

// System.String.GetHashCode(): http://referencesource.microsoft.com/#mscorlib/system/string.cs,0a17bbac4851d0d4
// System.Web.Util.StringUtil.GetStringHashCode(System.String): http://referencesource.microsoft.com/#System.Web/Util/StringUtil.cs,c97063570b4e791a
public static int CombineHashCodes(IEnumerable<int> hashCodes)

    int hash1 = (5381 << 16) + 5381;
    int hash2 = hash1;

    int i = 0;
    foreach (var hashCode in hashCodes)
    
        if (i % 2 == 0)
            hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ hashCode;
        else
            hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ hashCode;

        ++i;
    

    return hash1 + (hash2 * 1566083941);

另一个实现来自System.Web.Util.HashCodeCombiner.CombineHashCodes(System.Int32, System.Int32) 和System.Array.CombineHashCodes(System.Int32, System.Int32) 方法。这个比较简单,但可能没有上面的方法那么好分布:

// System.Web.Util.HashCodeCombiner.CombineHashCodes(System.Int32, System.Int32): http://referencesource.microsoft.com/#System.Web/Util/HashCodeCombiner.cs,21fb74ad8bb43f6b
// System.Array.CombineHashCodes(System.Int32, System.Int32): http://referencesource.microsoft.com/#mscorlib/system/array.cs,87d117c8cc772cca
public static int CombineHashCodes(IEnumerable<int> hashCodes)

    int hash = 5381;

    foreach (var hashCode in hashCodes)
        hash = ((hash << 5) + hash) ^ hashCode;

    return hash;

【讨论】:

【参考方案6】:

这是对 Special Sauce 精心研究的解决方案的重新包装。 它利用了值元组 (ITuple)。 这允许参数 seedfactor 的默认值。

public static int CombineHashes(this ITuple tupled, int seed=1009, int factor=9176)

    var hash = seed;

    for (var i = 0; i < tupled.Length; i++)
    
        unchecked
        
            hash = hash * factor + tupled[i].GetHashCode();
        
    

    return hash;

用法:

var hash1 = ("Foo", "Bar", 42).CombineHashes();    
var hash2 = ("Jon", "Skeet", "Constants").CombineHashes(seed=17, factor=31);

【讨论】:

【参考方案7】:

如果您的输入哈希大小相同,分布均匀且彼此不相关,则 XOR 应该没问题。而且速度很快。

我建议的情况是你想要做的地方

H = hash(A) ^ hash(B); // A and B are different types, so there's no way A == B.

当然,如果可以期望 A 和 B 以合理(不可忽略的)概率散列到相同的值,那么您不应该以这种方式使用 XOR。

【讨论】:

我如何判断我的哈希码是否均匀分布,是否有一个简单的基准可以做到这一点?我知道碰撞率很低,但这是否一定对应于均匀分布?【参考方案8】:

如果您正在寻找速度并且没有太多的碰撞,那么 XOR 是最快的。为了防止聚集在零附近,您可以执行以下操作:

finalHash = hash1 ^ hash2;
return finalHash != 0 ? finalHash : hash1;

当然,一些原型设计应该可以让您了解性能和集群。

【讨论】:

【参考方案9】:

假设你有一个相关的 toString() 函数(你的不同字段应该出现在哪里),我只返回它的哈希码:

this.toString().hashCode();

这不是很快,但应该可以很好地避免碰撞。

【讨论】:

【参考方案10】:

我建议使用 System.Security.Cryptography 中的内置哈希函数,而不是自己滚动。

【讨论】:

不,他们的目的非常不同,打破了 GetHashCode 应该快的规则。

以上是关于快速简单的哈希码组合的主要内容,如果未能解决你的问题,请参考以下文章

Java 中哈希码的说明

休眠:延迟初始化与损坏的哈希码/等于难题

哈希表、哈希算法、一致性哈希表

equals方法比较的是两个对象的哈希码,这么说对吗?

equals方法比较的是两个对象的哈希码,这么说对吗?

如何使用 Laravel 8 验证 MySQL 表中是不是存在哈希码