【问题标题】:What is the best way to implement this composite GetHashCode()实现此复合 GetHashCode() 的最佳方法是什么
【发布时间】:2011-02-13 13:57:47
【问题描述】:

我有一个简单的课程:

public class TileName {
    int Zoom, X, Y;

    public override bool Equals (object obj)
    {
        var o = obj as TileName;
        return (o != null) && (o.Zoom == Zoom) && (o.X == X) && (o.Y == Y);
    }

    public override int GetHashCode ()
    {
        return (Zoom + X + Y).GetHashCode();
    }
}

我很好奇如果我改为执行以下操作是否会获得更好的哈希码分布:

    public override int GetHashCode ()
    {
        return Zoom.GetHashCode() + X.GetHashCode() + Y.GetHashCode();
    }

这个类将用作字典键,所以我确实想确保有一个体面的分布。

【问题讨论】:

  • 小警告:请确保字段ZoomX 和'Y' 在创建类型后不能更改。实例的哈希码不得更改,否则将无法在您的哈希中找到键(我认为 FxCop 验证了这一点)。将调用 int Zoom, X, Y; 更改为 readonly int Zoom, X, Y; 以使其明显。

标签: c# .net computer-science hash


【解决方案1】:

就像 Jon Skeet in this SO answer 所描述的那样,最佳做法是选择一些素数并将它们与单个哈希码相乘,然后将所有内容相加。

public int GetHashCode()
{
    unchecked
    {
        int hash = 17;
        // Maybe nullity checks, if these are objects not primitives!
        hash = hash * 23 + Zoom.GetHashCode();
        hash = hash * 23 + X.GetHashCode();
        hash = hash * 23 + Y.GetHashCode();
        return hash;
    }
}

xor 哈希的问题是:

  • 如果X 等于Y,那么你的哈希值就是Zoom,因为X ^ Y = X ^ X = 0 成立
  • xor 是一个对称运算符,它将为 [Zoom = 3, X = 5, Y = 7][Zoom = 3, X = 7, Y = 5][Zoom = 7, X = 5, Y = 3] 等对象生成完全相同的哈希。

这些事实使异或方法更容易引起冲突。

除了 Jons 的帖子,考虑使用 unchecked 上下文,以明确忽略溢出。因为就像MSDN 所说:

如果checkedunchecked 都不是 使用时,常量表达式使用 编译时的默认溢出检查 时间,这是检查。否则,如果 表达式是非常数的, 运行时溢出检查取决于 其他因素,例如编译器选项 和环境配置。

因此,虽然通常不会检查溢出,但它可能会在某些环境中或使用某些编译器选项构建时失败。但在这种情况下,您希望明确不检查这些溢出。

更新:

顺便说一句:someInt.GetHashCode() 返回someInt。像这样,它当然是最快的和完美的哈希分布,没有一次碰撞。否则您将如何将 int 映射到 int-hash? :) 所以我想说的是:你的第一种方法:

return (Zoom + X + Y).GetHashCode();

还有你的第二个:

return Zoom.GetHashCode() + X.GetHashCode() + Y.GetHashCode();

完全一样。您甚至不必致电GetHashCode,两者都很可能发生冲突。可能比xor 方法更糟糕,如果您很可能对所有三个整数都有小整数值。

更新 2:

正如我在 ChaosPandions 帖子的评论中所写:如果您只有这三个 int 值,并且 XYZoom 是相对较小的数字(小于 1000 或 10000),这可能也是一个好的哈希生成器:

public int GetHashCode()
{
    return (X << 16) ^ (Y << 8) ^ Zoom;
}

它只是分配散列值中的位(为了便于阅读,以 big-endian 为例):

00000000 00000000 00000011 00110001    X = 817
00000000 00000000 00011011 11111010    Y = 7162
00000000 00000000 00000010 10010110    Zoom = 662

00000011 00110001 00000000 00000000    X << 16
00000000 00011011 11111010 00000000    Y << 8
00000000 00000000 00000010 10010110    Zoom

00000011 00101010 11111000 10010110    (X << 16) ^ (Y << 8) ^ Zoom

【讨论】:

    【解决方案2】:

    您问题中的两种实现都不理想。例如,它们将为{ Zoom=1, X=2, Y=3 }{ Zoom=2, X=3, Y=1 }{ Zoom=3, X=1, Y=2 } 等返回完全相同的哈希。

    我通常使用这样的东西:

    public override int GetHashCode()
    {
        // 269 and 47 are primes
        int hash = 269;
        hash = (hash * 47) + Zoom.GetHashCode();
        hash = (hash * 47) + X.GetHashCode();
        hash = (hash * 47) + Y.GetHashCode();
        return hash;
    }
    

    (根据记忆,我认为 C# 编译器在为匿名类型生成 GetHashCode 方法时使用了类似的东西。)

    【讨论】:

    • @Philip:我以前看过 Jon 提到它,但我不记得我最初是在哪里捡到它的。我认为这是一个相当普遍的实现。
    • 是的,这只是一个好习惯,应该让更多人习惯。
    【解决方案3】:

    我知道这个问题有点老了,但现在您可以使用 System.HashCode 类轻松创建哈希码

    https://docs.microsoft.com/en-us/dotnet/api/system.hashcode.combine?view=netcore-3.1

    在这种特定情况下,它看起来像

    public override int GetHashCode()
    {
        return HashCode.Combine(Zoom, X, Y);
    }
    
    

    【讨论】:

      【解决方案4】:

      我发现这确实很有效。

      public override int GetHashCode ()
      {
          return Zoom.GetHashCode() ^ X.GetHashCode() ^ Y.GetHashCode();
      }
      

      【讨论】:

      • 虽然这比问题中的实现要好,但仍然不是很好。例如,它不考虑字段排序,因此{ Zoom=1, X=2, Y=3 }{ Zoom=2, X=3, Y=1 }{ Zoom=3, X=1, Y=2 } 等都会返回相同的哈希值。某种滚动乘法和/或求和将避免这种情况(并且可能也会提供更好的分布)。
      • @Luke:同意。 @ChoasPandion:请在此处阅读:stackoverflow.com/questions/263400/…
      • @Luke - 我同意,通常我会尝试使用最简单的解决方案来解决任何问题。对于任何严肃的应用程序,您都需要使用碰撞几率较小的算法。
      • @ChaosPandion:Jon Skeets 解决方案同样简单,碰撞的可能性更小。它不像那是一个非常复杂的大型算法。如果您不关心碰撞,您可以为每个实例静态地return 1;。好吧,开个玩笑……:D
      • 刚想出一个新点子,可能是我们两种方案的折衷方案:如果你只有这三个int值,而XYZoom是比较小的数字(小于 1000 或 10000)这个也可能是一个很好的哈希生成器:return (X &lt;&lt; 16) ^ (Y &lt;&lt; 8) ^ Zoom;
      【解决方案5】:
      public override int GetHashCode ()
      {
          return (Zoom.ToString() + "-" + X.ToString() + "-" + Y.ToString()).GetHashCode();
      }
      

      【讨论】:

      • 这可能会提供一个很好的分布,但对于性能来说确实很糟糕,因为每次调用 GetHashCode 时至少会创建一个新字符串和一个新字符串数组。你宁愿分布不好。
      • @Steven,这可以在计算后缓存,并在任何设置缩放、X 或 Y 时清除缓存的值。
      • @Fede:您可以缓存慢速算法的结果,也可以只使用快速算法。顺便说一句:缓存只有在您有只读字段时才有意义,或者您也必须存储字段旧值。那会变得一团糟……
      • @Philip:您不需要存储旧值。您可以将 GetHashCode 的结果缓存在可为空的 int 中。如果缓存为空,则计算它,如果不是,则返回该值。设置影响缓存的字段时,只需将缓存设置为空。当操作成本乘以将被调用的次数是瓶颈的原因时,缓存是有意义的。
      • OP 确实要求进行体面的分发。性能可能是一个问题,但我们正在查看什么大小的数据集?
      猜你喜欢
      • 2021-06-06
      • 1970-01-01
      • 1970-01-01
      • 2018-02-10
      • 2010-09-20
      • 2020-10-01
      • 2020-01-15
      • 2010-09-09
      • 2015-08-21
      相关资源
      最近更新 更多