良好的GetHashCode()覆盖了关于订单的Foo对象列表

Ben*_* B. 30 c# algorithm hashcode gethashcode

EnumerableObject : IEnumerable<Foo>

包裹一个 List<Foo>

如果EnumerableObject a.SequenceEquals( EnumerableObject b),那么他们是平等的.

因此,GetHashCode必须实施.问题是XORing列表中的每个元素将返回任何列表的相同哈希码,所有列表都包含所有且只有相同的元素,而不管顺序如何.就工作而言,这是好的,但会导致许多冲突,这将减慢检索速度等.

GetHashCode对于依赖于顺序的对象列表,什么是一种好的,快速的方法?

Jon*_*eet 62

我的方式与通常组合哈希码的方式相同 - 加法和乘法:

public override int GetHashCode()
{
    unchecked
    {
        int hash = 19;
        foreach (var foo in foos)
        {
            hash = hash * 31 + foo.GetHashCode();
        }
        return hash;
    }
}
Run Code Online (Sandbox Code Playgroud)

(请注意,在任何描述的哈希表中使用此键后,不应向列表中添加任何内容,因为哈希值会更改.这也假设没有空条目 - 如果可能,则需要考虑到这一点.)

  • 因为List <T>相等不是序列相等. (7认同)
  • @MK_Dev:嗯,它们都是素数,这通常有帮助.我承认不理解为什么这个哈希通常运作良好的数学背后的数学.乘法乘31可以轻松优化移位和减法,这使其具有吸引力.有一个页面在某处解释这种哈希,但我不记得在哪里,我害怕:( (5认同)
  • 奇怪的是,`List <T>``GetHashCode`的实现只是继承自`object`. (3认同)
  • @Jon你可能是指[本页](http://eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx),你在[这个答案]中提到过(http://stackoverflow.com/a/263416)/399124). (3认同)
  • @nawfal:说实话,我宁愿不分散一般方法.希望任何拥有包含空引用的集合的人都可以搞清楚... (2认同)
  • @MuhammadRehanSaeed:“空集合”是什么意思?您无法对空集合调用“GetHashCode”。如果“foos”为空,则答案中的代码将引发异常。如果它*可以*为空,那么该方法应该考虑到这一点。null 和empty 是否应该被视为相等是一个特定于上下文的决定。 (2认同)

Jon*_*nna 13

首先,仔细检查您是否需要哈希码.您是否要将这些列表放入哈希映射结构(例如字典,哈希集等)?如果没有,请忘掉它.

现在,假设您的意思是EnumerableObject 由于某种原因已经覆盖Equals(object)(并且希望因此也实现IEquatable<EnumerableObject>),那么这确实是必要的.您希望平衡速度与位分布.

一个好的起点是mult + add或shift + xor,如:

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    foreach(var item in this)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}
Run Code Online (Sandbox Code Playgroud)

(这假设您正在使用item.Equals()进行序列相等性比较,如果您使用的是IEqualityComparer,则需要调用其哈希码).

从那里我们可以优化.

如果不允许使用null项,则删除null-check(注意,如果代码确实找到null,这将使代码抛出).

如果非常大的列表很常见,我们需要减少检查的数量,同时尽量不要导致大量的冲突.比较以下不同的实现:

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int max = Math.Min(Count, 16);
    for(int i = 0, i != max; ++i)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int min = Math.Max(-1, Count - 16);
    for(int i = Count -1, i != min; --i)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}

public override int GetHashCode()
{
    int res = 0x2D2816FE;
    int step = Count / 16 + 1;
    for(int i = 0, i < Count; i += step)
    {
        var item = this[i];
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
}
Run Code Online (Sandbox Code Playgroud)

这些中的每一项都限制了所检查的项目总数,从而加快了执行速度,但却存在较差的质量哈希值.哪个(如果有的话)最好取决于具有相同开头或相同结尾的集合是否更有可能.

更改上面的数字16会调整余额; 较小但速度较快但散列质量较高,散列冲突风险较低.

编辑:现在你可以使用我的SpookyHash v.2的实现:

public override int GetHashCode()
{
  var hasher = new SpookyHash();//use methods with seeds if you need to prevent HashDos
  foreach(var item in this)
    hasher.Update(item.GetHashCode());//or relevant feeds of item, etc.
  return hasher.Final().GetHashCode();
}
Run Code Online (Sandbox Code Playgroud)

这将创建比mult + add或shift + xor更好的分布,同时也特别快(特别是在64位进程中,因为算法针对此进行了优化,尽管它也适用于32位).

  • 你能解释一下“0x2D2816FE”来自哪里吗?Google 建议这是 .Net 的空字符串哈希码,但我不确定为什么这是一个很好的起始值。Jon Skeet 的答案以类似的方式使用了“19”,但我也不明白。 (2认同)

Mov*_*GP0 7

.GetHashCode()方法通常只返回基于对象引用(指针地址)的哈希值。这是因为计算可枚举列表中每个项目的哈希码可能非常耗时。与其覆盖现有行为,我更喜欢使用扩展方法,并且仅在需要确定性地确定哈希码时才使用它:

public static class EnumerableExtensions
{
    public static int GetSequenceHashCode<TItem>(this IEnumerable<TItem> list)
    {
        if (list == null) return 0;
        const int seedValue = 0x2D2816FE;
        const int primeNumber = 397;
        return list.Aggregate(seedValue, (current, item) => (current * primeNumber) + (Equals(item, default(TItem)) ? 0 : item.GetHashCode()));
    }
}
Run Code Online (Sandbox Code Playgroud)