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

时间:2011-11-11 13:49:50

标签: c# algorithm hashcode gethashcode

EnumerableObject : IEnumerable<Foo>

包装List<Foo>

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

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

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

3 个答案:

答案 0 :(得分:56)

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

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

(请注意,在任何描述的哈希表中使用此键后,不应向列表中添加任何内容,因为哈希值会发生变化。这也假设没有空条目 - 如果可以的话是的,你需要考虑到这一点。)

答案 1 :(得分:12)

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

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

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

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

(这假设您正在使用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;
}

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

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

编辑:现在您可以使用我的implementation of 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();
}

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

答案 2 :(得分:4)

.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()));
    }
}