快速简单的哈希码组合


70

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

围绕SO和Web进行阅读似乎有一些主要的候选人:

  1. 异或
  2. 使用素数乘法进行异或
  3. 简单的数字运算,例如乘法/除法(带有溢出检查或环绕)
  4. 生成一个String,然后使用String类的Hash Code方法

人们会推荐什么,为什么?

Answers:


120

我个人会避免XOR-这意味着任何两个相等的值都将导致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,没有人真正知道为什么它行之有效,但确实如此。它也很容易记住,易于实现,并且易于扩展到任意数量的字段。


4
看起来像丹·伯恩斯坦(或克里斯·托雷克)的哈希,只是具有不同的常数。谁也不知道为什么效果很好。
迅速

11
值得一提的是,这是Berstein哈希表的(变体),并且因为没人知道为什么它在测试中表现出色,所以在哈希表很关键时不建议这样做。请参阅eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx。另外,您应该将此代码包装在一个unchecked { }块中。GetHashCode()不应引发任何异常。
Henk Holterman

5
@tofutim:31是一个不错的选择,因为可以将31的乘法优化为移位和减法。是否以这种方式进行优化取决于平台。至于为什么这些数字可以很好地用于哈希运算-正如Henk所说,这有点神秘。
乔恩·斯基特

3
@ rory.ap:我认为这是一项出色的工作,我非常乐意使用这些数字。尽管我讨厌承认“因为其他人说过”而使用常量,但这基本上就是17/31对。
乔恩·斯基特

12
从.NET Core 2.1开始,您可以使用System.HashCode类型的Combine方法执行此操作docs.microsoft.com/en-us/dotnet/api/system.hashcode.combine
Cosmin Sontu

52

尽管在Jon Skeet的答案中概述的模板通常可以很好地作为哈希函数系列使用,但常量的选择很重要,答案中指出的种子17和因数31对于普通用例根本不起作用。在大多数使用情况下,散列的值比都更接近零int.MaxValue,并且共同进行散列的项目数不超过几十个。

对于散列一个整数的元组{x, y},其中-1000 <= x <= 1000-1000 <= y <= 1000,它有将近98.5%的深不可测的碰撞率。例如{1, 0} -> {0, 31}{1, 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;
}

我写了一个蒙特卡洛采样搜索循环,用随机种子的各个随机n元组的各种种子和因子值测试了上述方法i。允许的范围为2 <= n <= 25n是随机的,但偏向范围的下限)和-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%的范围内,我认为不应使用。


8
哇。我喜欢这个答案的工作。令人印象深刻,做得好!
汤姆·

似乎是最好的,但有两句话:首先是简单的一点,为什么不将“ seed”和“ factor”移到最后,并给它们提供默认值(1009和9176),它应为大多数人民使用。第二点:就像Jon Skeet算法一样,它依赖于顺序,如果以不同的顺序进给,则可以获得不同的哈希值。我想知道,如果以不同的方式喂食算法,首先对数组进行排序以确保最后具有相同的最终哈希值是否更安全。这样会更安全。
埃里克·厄勒

1
@EricOuellet因为params int[] vals必须位于所有函数参数的末尾,所以我无法设置seedfactor默认参数。如果您不关心params语法的便利性,则可以将其删除,然后对参数重新排序以允许按照建议使用默认值。
Special Sauce

@EricOuellet数组的默认哈希应考虑排列(这是更一般的情况),因此哈希对于不同的顺序将有所不同(就像字符串“ abc”的哈希与“ acb”的哈希不同一样) )。如果您特别希望仅将哈希函数用于组合,则可能应接受一个HashSet<int>参数(假设没有重复项)。否则,您可以重命名该功能以CustomHashCombination()防止混淆,并按照建议进行内部预排序。
Special Sauce

1
我喜欢这个答案,但我不会使用,params因为它必须在每次调用时分配一个数组。因此,它可能是更快的计算方法,但会为以后创建GC压力。
Gru

50

如果您使用的是.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 Core 2.1 / .NET Standard 2.1开始的.NET本身的一部分(尽管请参阅下面的con)
    • 对于.NET Framework 4.6.1和更高版本,Microsoft.Bcl.HashCode NuGet包可用于向后移植此类型。
  • 根据作者和审稿人在合并到corefx存储库中之前所做的工作,看起来具有良好的性能和混合特性。
  • 自动处理空值
  • 需要IEqualityComparer实例的重载

缺点:


您可以参考nuget.org/packages/Microsoft.Bcl.HashCode使其正式在.NET Framework 4.6.1或.NET Standard 2.0上运行。
ChrisTorng '20

19

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

(field1, field2).GetHashCode();

好主意,尽管我怀疑这可能与GC流失有关,因为您隐式地创建了一个短暂存在的对象
RobV

9
@RobV元组是值类型,因此它们是堆栈分配的,并且不施加GC压力。
Mike Pedersen

1
一个问题...(0,1,2).GetHashCode()和(0,0,1,2).GetHashCode()都产生相同的值:35。而在最受支持的答案中的方法产生唯一值0 ,1、2:506480和0、0、1、2:15699890
dynamichael

2
哈希代码不保证唯一。您发现了一个并非如此的情况...除非有很多冲突,否则这并不是一个不好的选择(在这种情况下,提交错误是个好主意)。我个人更喜欢使用框架中的内容,而不是实现不同的内容。
Yepeekai

1
它实际上ValueTuple是结构(MSDN)的类型。请注意,该Tuple类型是一类,并且具有GC压力。我喜欢这样 在内部它类似于@Stipo的帖子,但非常易于理解和查看。在大多数情况下,这将是一个不错的选择。
cactuaroid

17

我假设.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;
}

2

这是Special Sauce出色研究解决方案的重新包装。
它利用值元组(ITuple)。
这允许使用参数seed和的默认值factor

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);

0

如果您的输入哈希大小相同,分布均匀且彼此无关,那么XOR应该可以。另外它很快。

我建议这样做的情况是您要执行的操作

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

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


我怎么知道我的哈希码是否均匀分布,对此是否有一个简单的基准?我知道碰撞率很低,但这是否必然对应于均匀分布?
RobV

0

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

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

当然,一些原型应使您对性能和群集有所了解。


-1

假设您有一个相关的toString()函数(将在其中显示不同的字段),我只返回其哈希码:

this.toString().hashCode();

这不是很快,但是应该很好地避免冲突。


-11

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


11
不,它们的目的截然不同,并打破了GetHashCode应该快速的规则。
汉克·霍尔特曼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.