【问题标题】:What would be a good hashCode for a DateRange class对于 DateRange 类,什么是好的 hashCode
【发布时间】:2010-08-19 20:31:59
【问题描述】:

我有以下课程

public class DateRange
{
    private DateTime startDate;
    private DateTime endDate;
    public override bool Equals(object obj)
    {
        DateRange other = (DateRange)obj;
        if (startDate != other.startDate)
            return false;
        if (endDate != other.endDate)
            return false;
        return true;
    }
    ...
}

我需要将一些值存储在一个以 DateRange 为键的字典中,例如:

Dictionary<DateRange, double> tddList;

我应该如何覆盖DateRange类的GetHashCode()方法?

【问题讨论】:

  • 如果给定 null 或非 DateRange 对象,您的 Equals 方法应该返回 false,而不是崩溃。
  • 它也可以与 impl 一起使用。 IEquatable,然后 Equals(object) 覆盖可以只返回 Equals(other as DateRange),如果 querant 期望这是一个经常足以担心 GetHashCode() 的键值。

标签: c# data-structures dictionary gethashcode


【解决方案1】:

我使用 Effective Java 中的这种方法来组合哈希:

unchecked
{
    int hash = 17;
    hash = hash * 31 + field1.GetHashCode();
    hash = hash * 31 + field2.GetHashCode();
    ...
    return hash;
}

没有理由在这种情况下不能正常工作。

【讨论】:

  • @Jon:这很有趣。所以我认为这个哈希是均匀分布最好的?
  • 我已经看到了将哈希乘以素数并添加图像处理代码中使用的另一个哈希的相同原理,因此必须有充分的理由这样做,但这一直困扰着我. (续...)
  • 由于散列位在某种程度上是均匀分布的,因此乘以 1 会将一些位移出散列容器的高端(在本例中为 int)。我意识到在向固定大小的容器中添加位时这是不可避免的,但是较早的散列(在本例中为 field1)将移出更多位,并且最终散列(在本例中为 field2)的所有位都将被保留。这不会使这项技术有些不可靠吗?如果组合足够多的哈希值以将早期值的所有位完全移出容器会怎样?
  • @Dour High Arch:我承认我不知道为什么这行得通的细节......我只是有一个相当可靠的权威,它是一个非常好的将军目的哈希算法。不过,我有兴趣看到更多分析。
  • @Jon 如果已编译选中,这将在许多可能的值上引发 System.OverflowException。也许添加一个明确的未经检查的块?
【解决方案2】:

这取决于我希望看到它使用的值。

如果它通常具有不同的日期值,而不是同一天的不同时间,并且它们在现在的一个世纪之内,我会使用:

unchecked
{
    int hash = startDate.Year + endDate.Year - 4007;
    hash *= 367 + startDate.DayOfYear;
    return hash * 367 + endDate.DayOfYear;
}

这可以很好地分配具有预期值的位,同时减少移位中丢失的位数。请注意,虽然在某些情况下,对质数的依赖在冲突时可能会出奇地糟糕(尤其是当哈希被馈送到使用相同质数的模的东西中以试图避免在产生更小的哈希以在其桶之间分配时发生冲突时) 我选择了高于更明显选择的素数,因为它们只是在上面,所以对于比特分布来说仍然非常“紧”。我不太担心两次使用相同的素数,因为它们以这种方式非常“紧密”,但如果你有一个包含 367 个桶的基于散列的集合,它确实会受到伤害。这很好地(但不是很好)处理过去或未来的日期,但如果假设同一天(时间不同)内将有很少或没有范围的假设是错误的,因为该信息完全丢失,那么这是可怕的。

如果我期待(或为其他方的一般用途而写作,并且无法假设),我会选择:

int startHash = startDate.GetHashCode();
return (((startHash >> 24) & 0x000000FF) | ((startHash >> 8) & 0x0000FF00) | ((startHash << 8) & 0x00FF0000) | (unchecked((int)((startHash << 24) & 0xFF000000)))) ^ endDate.GetHashCode();

第一种方法的工作原理是假设 DateTime 中的通用 GetHashCode 不如我们想要的那么好,这个方法取决于它是否良好,但会混合一个值的位。

它可以很好地处理更明显的棘手情况,例如两个值相同或彼此相距相同的距离(例如很多 1 天或 1 小时的范围)。在第一个示例效果最好的情况下,它并没有那么好,但是如果有很多范围使用同一天但不同的时间,那么第一个示例就完全糟糕了。


编辑:更详细地回应 Dour 的担忧:

Dour 正确地指出,此页面上的某些答案会丢失数据。事实上,它们都丢失了数据。

问题中定义的类有 8.96077483×1037 个不同的有效状态(如果我们不关心每个日期的 DateTimeKind,则为 9.95641648×1036) , GetHashCode 的输出有 4294967296 种可能的状态(其中之一 - 零 - 也将用作空值的哈希码,通常可以与实际代码进行比较)。无论我们做什么,我们都会以 2.31815886 × 1027 的比例减少信息。我们丢失了很多信息!

我们可能会在某些方面比在其他方面失去更多,这可能是真的。当然,通过编写一个有效但非常糟糕的答案,很容易证明某些解决方案可能比其他解决方案损失更多。

(更糟糕的有效解决方案是return 0;,它是有效的,因为它在相等的对象上永远不会出错或不匹配,但尽可能差,因为它会碰撞所有值。基于散列的集合的性能变为 O(n ),并且随着 O(n) 的进行而变慢,因为所涉及的常数比搜索无序列表这样的 O(n) 操作要高。

很难衡量损失了多少。考虑到 XOR 将剩余的信息量减半,在 XORing 之前移位某些位比交换位损失多少。即使是天真的x ^ y 也不会比交换和异或损失更多,它只是在共同值上发生了更多的冲突; swap-and-xor 会在plain-xor 没有的值上发生冲突。

一旦我们在不丢失更多信息但返回 4294967296 或接近 4294967296 个可能值且这些值之间分布良好的解决方案之间做出选择,那么问题就不再是多少信息丢失了(答案只剩下4.31376821×10-28个原始信息)但是哪些信息丢失了。

这就是我上面的第一个建议忽略时间组件的原因。一天中有 864000000000 个“滴答声”(DateTime 的分辨率为 100 纳秒单位),我故意丢弃其中的两块滴答声(两者之间的可能值是 7.46496×1023),因为我正在考虑一个无论如何都不使用该信息的场景。在这种情况下,我特意构建了机制,以选择 哪些 信息丢失,从而改善给定情况下的哈希值,但如果我们有不同的值,它绝对毫无价值开始和结束日期不在同一天,而是在不同的时间。

同样,x ^ y 不会比其他任何一个丢失更多的信息,但它确实丢失的信息比其他选择更可能是重要的。

如果没有任何方法可以预测哪些信息可能很重要(尤其是如果您的课程是公开的并且其哈希码被外部代码使用),那么我们可以安全地做出的假设会受到更多限制.

总的来说,prime-mult 或 prime-mod 方法在丢失信息方面比基于移位的方法更好,但具有讽刺意味的是,当在基于哈希的方法中可能发生的进一步哈希中使用相同的素数时考虑到相同的目标(没有数字与自身相对质数!甚至质数)在这种情况下,它们会更糟。另一方面,如果将基于移位的方法输入到基于移位的进一步哈希中,则基于移位的方法确实会失败。任意数据和任意使用都没有完美的散列(除非一个类的有效值很少并且我们将它们全部匹配,在这种情况下,它是一种比我们生成的散列更严格的编码)。

简而言之,无论做什么,您都会丢失信息,重要的是您丢失了哪个

【讨论】:

  • 您的第二种算法解决了我对某些组件比其他组件丢失更多数据的担忧。好主意。
  • @Dour,它有其优点和缺点,我认为你夸大了丢失数据的问题,因为我在我编辑的答案中更详细地详细说明了。
  • 我明白丢弃数据是不可避免的。我担心的是,某些方法会从同一哈希中使用的每条数据中丢失不同的数量。考虑 Rob 哈希的迭代版本,每次迭代移动 4 位。这将丢弃除所考虑的最后八个日期之外的所有日期的所有位,将仅包括第八个日期的低 8 个位,依此类推,直到它包括所考虑的最终日期的所有位。我同意这是您丢失的“哪些”数据,但它应该从散列中使用的每个数据中丢失大致相等的数量; shift 和 multiply 方法不这样做。
  • 不,移位和乘法 do 损失的数量大致相等,除非您发现上面的数学有问题。出于您给出的原因,Rob 哈希的迭代版本确实不是一个好主意,这是丢失和浪费的情况(如果您要放弃大部分输入,请不要浪费时间寻找在它)。 Rob 的散列虽然没有被迭代。我上面的字节交换方法也不能很好地迭代许多项目,尽管我敢打赌它会在避免碰撞时击败 Rob。如果我们需要输入许多项目,我们会使用模数方法。 Rob 和我都很接近……
  • ... 可能保留的信息总量,但不同的是它是什么。或者,如果您有 3 条数据,其中 2 条很少变化,而 1 条变化很大,请继续删除很少变化的 2!尤其是数量并不重要。一旦你已经达到了 2³² 个可能的结果。
【解决方案3】:

好吧,考虑一下一个好的散列函数应该具备哪些特征。它必须

  • 与 Equals 一致 - 也就是说,如果两个对象的 Equals 为真,那么这两个哈希码也必须相同。
  • 永不崩溃

而且它应该

  • 非常快
  • 为相似的输入给出不同的结果

我要做的是想出一个非常简单的算法;比如说,从第一个哈希码中取出 16 位,从第二个哈希码中取出 16 位,然后将它们组合在一起。让自己成为代表性样本的测试用例;可能实际使用的日期范围,看看这个算法是否给出了良好的分布。

一个常见的选择是将两个散列异或在一起。对于这种类型,这不一定是一个好主意,因为似乎有人想要表示从 X 到 X 的零长度范围。如果你对两个相等的 DateTimes 的哈希值进行异或运算,你总是得到零,这看起来像一个很多哈希冲突的秘诀。

【讨论】:

    【解决方案4】:

    您必须移动范围的一端,否则两个相等的日期将散列为零,我想这是一种很常见的情况:

    return startDate.GetHashCode() ^ (endDate.GetHashCode() << 4);
    

    【讨论】:

    • 移位 4 意味着丢弃 4 位信息。虽然日期可能没有问题(取决于 DateTime.GetHashCode() 的实现方式),但更好的方法是使用带素数和加法的乘法(参见 Jon Skeet 的回答)。
    • @Daniel:谢谢。 Jon 的答案现在是我的实用程序库的一部分。
    • @Daniel,所有哈希,除非您可以将表示的所有位都放入哈希大小中(例如在 Int16.GetHashCode() 中)会丢弃一些信息。常用的 shift、prime mult 和 prime mod 方法各有利弊。即使 shift-rotate(它“带回”丢弃的位)仍然会丢失后面的 + 或 ^ 中的信息。
    【解决方案5】:
    return startDate.GetHashCode() ^ endDate.GetHashCode();
    

    可能是一个好的开始。当 startDate 和 endDate 之间的距离相等但日期不同时,您必须检查是否获得了良好的分布。

    【讨论】:

    • 坏主意 - 那么在同一日期开始和结束的 每个 日期范围将具有相同的哈希码 0。(但由于感觉有点刺耳,所以删除了反对票。)
    猜你喜欢
    • 2022-12-12
    • 1970-01-01
    • 2013-04-05
    • 1970-01-01
    • 2017-05-17
    • 1970-01-01
    • 2015-01-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多