在列表上创建哈希值?

时间:2011-09-02 00:33:40

标签: c# collections .net-4.0 hash queue

我有一个List<MyRichObject>,里面有50个实例。每个实例都有1或2个唯一属性,但在某种程度上它们都是唯一的,因为列表中只有一个位置,等等。

我想提出一种独特的方法来“散列”此列表,因此它与所有其他列表都是唯一的。在.NET 4中有一种聪明的方法吗?

目的是为列表创建一种“monniker”,以便将它们转储到队列中,稍后根据它们的独特价值找到它们。

感谢。

2 个答案:

答案 0 :(得分:25)

TL; DR

public static int GetSequenceHashCode<T>(this IList<T> sequence)
{
    const int seed = 487;
    const int modifier = 31;

    unchecked
    {
        return sequence.Aggregate(seed, (current, item) =>
            (current*modifier) + item.GetHashCode());
    }            
}

为什么要打扰另一个答案?

如果列表中的多个项目具有相同的哈希码,则accepted answer会给出危险的不准确结果。例如,考虑这些输入:

var a = new []{ "foo" };
var b = new []{ "foo", "bar" };
var c = new []{ "foo", "bar", "spam" };
var d = new []{ "seenoevil", "hearnoevil", "speaknoevil" };

这些都产生了不同的结果,表明它们都是独特的收藏品。大!现在让我们尝试重复:

var e = new []{ "foo", "bar", "spam" };

GetSequenceHashCode应该为ce生成相同的结果 - 而且确实如此。到现在为止还挺好。现在让我们尝试不按顺序的项目:

var f = new []{ "spam", "bar", "foo" };

哦,哦...... GetSequenceHashCode表示f等于ce。为什么会这样?首先将c作为示例分解为实际的哈希码值:

int hashC = "foo".GetHashCode() ^ 
            "bar".GetHashCode() ^ 
            "spam".GetHashCode();

由于这里的确切数字并不重要,为了更清晰的演示,让我们假装三个字符串的哈希码是foo=8bar=16spam=32。所以:

int hashC = 8 ^ 16 ^ 32;

或将其分解为二进制表示:

8 ^ 16 ^ 32 == 56;

//  8 = 00001000
//  ^
// 16 = 00010000
//  ^
// 32 = 00100000
//  =
// 56   00111000

现在您应该了解为什么此实现忽略了列表中项目的顺序,即8^16^32 = 16^8^32 = 32^16^8等。

其次是重复问题。即使你假设在不同的序列中具有相同的内容是可以的(这不是我鼓励的方法),我认为没有人会认为下面的行为是可取的。让我们尝试在每个列表中重复的变体。

var a = new []{ "foo", "bar", "spam" };
var b = new []{ "foo", "bar", "spam", "foo" };
var c = new []{ "foo", "bar", "spam", "foo", "foo" };
var d = new []{ "foo", "bar", "spam", "foo", "foo", "spam", "foo", "spam", "foo" };

虽然ab生成了不同的序列哈希,GetSequenceHashCode表明acd都是相同的。为什么?

如果您对自己的数字进行异或,则基本上将其取消,即

8 ^ 8 == 0;

//  8 = 00001000
//  ^
//  8 = 00001000
//  =
//  0 = 00000000

再次使用相同数字的XOR给出原始结果,即

8 ^ 8 ^ 8 == 8;

//  8 = 00001000
//  ^
//  8 = 00001000
//  ^
//  8 = 00001000
//  =
//  8 = 00001000

因此,如果我们再次查看ac,请替换简化的哈希码:

var a = new []{ 8, 16, 32 };
var c = new []{ 8, 16, 32, 8, 8 };

哈希码被公式化为:

int hashA = 8 ^ 16 ^ 32;         // = 56
int hashC = 8 ^ 16 ^ 32 ^ 8 ^ 8; // = 56
                       // ↑   ↑ 
                       // these two cancel each other out

同样使用d,其中每对foospam都会自行取消。

答案 1 :(得分:2)

哈希是否必须代表列表的内容?换句话说,您将使用哈希来确定潜在的相等性吗?如果没有,那么只需创建一个新的Guid并使用它。

如果标识符确实需要表示列表的内容,那么您可以根据列表的内容生成哈希码(这将是低效的,因为您将无法缓存此值,因为列表的内容可能会更改)或者完全放弃哈希并使用Enumerable.SequenceEquals来确定相等。


以下是我如何实现获取List<T>的哈希码的示例。首先,如果您要获取特定对象的哈希码,您应该确保该对象不会更改。如果该对象确实发生了变化,那么您的哈希码就不再有用了。

使用可以“冻结”的列表(意味着在某个点之后没有添加或删除项目)的最佳方法是调用AsReadOnly。这将为您提供ReadOnlyCollection<T>。下面的实现取决于ReadOnlyCollection<T>只是为了安全,所以请记住:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

class Example
{
    static void Main()
    {
        var seqOne = new List<int> { 1, 2, 3, 4, 5, 6 };
        var seqTwo = new List<int> { 6, 5, 4, 3, 2, 1 };

        var seqOneCode = seqOne.AsReadOnly().GetSequenceHashCode();
        var seqTwoCode = seqTwo.AsReadOnly().GetSequenceHashCode();

        Console.WriteLine(seqOneCode == seqTwoCode);
    }
}

static class Extensions
{
    public static int GetSequenceHashCode<T>(this ReadOnlyCollection<T> sequence)
    {
        return sequence
            .Select(item => item.GetHashCode())
            .Aggregate((total, nextCode) => total ^ nextCode);
    }
}

哦,最后一件事 - 请确保您的MyRichObject类型has a good GetHashCode implementation itself否则您的列表哈希码可能会在比较时产生大量误报。