繁体   English   中英

快速简单的哈希码组合

[英]Quick and Simple Hash Code Combinations

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

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

  1. 异或
  2. 质数乘法异或
  3. 简单的数字运算,如乘法/除法(带有溢出检查或环绕)
  4. 构建一个字符串,然后使用字符串类哈希代码方法

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

我个人会避免 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,没有人真正知道为什么它运行良好,但确实如此。 它也易于记忆、易于实现,并且易于扩展到任意数量的领域。

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

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

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 开始(不过,请参阅下面的 con)
    • 对于 .NET Framework 4.6.1 及更高版本, Microsoft.Bcl.HashCode NuGet 包可用于向后移植此类型。
  • 基于作者和审阅者在将其合并到 corefx 存储库之前所做的工作,看起来具有良好的性能和混合特性
  • 自动处理空值
  • 采用IEqualityComparer实例的重载

缺点:

虽然 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;
}

我编写了一个 Monte Carlo 采样搜索循环,该循环使用各种随机整数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 ),它们的表现都比上述解决方案差。

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

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

(field1, field2).GetHashCode();

我认为 .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;
}

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

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

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

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

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

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

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

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

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

this.toString().hashCode();

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM