在一个列表上创建哈希值?
问题:在一个列表上创建哈希值的原因和解决方法
哈希是否代表列表的内容?换句话说,你是否使用哈希来确定可能的相等性?如果不是的话,只需创建一个新的 GUID 并使用它。
如果标识符确实需要表示列表的内容,那么您可以根据列表的内容生成一个哈希码(这将是低效的,因为您将无法缓存此值,因为列表的内容可能会更改),或者放弃哈希,并使用 Enumerable.SequenceEquals 来确定相等性。
下面是我实现获取 List
与可以“冻结”(即在某一点之后不能添加或删除项)的列表一起工作的最佳方法是调用 AsReadOnly。这将为您提供一个 ReadOnlyCollection
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; class Example { static void Main() { var seqOne = new List{ 1, 2, 3, 4, 5, 6 }; var seqTwo = new List { 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 (this ReadOnlyCollection sequence) { return sequence .Select(item => item.GetHashCode()) .Aggregate((total, nextCode) => total ^ nextCode); } }
另外,确保您的 MyRichObject 类具有良好的 GetHashCode 实现,否则列表的哈希码在比较时可能会产生许多误报。
谢谢。这不是为了确定相等性,而是为了创建一个基于列表内容唯一的值。我创建了 500 个这样的列表并将它们放入队列中,我想检查队列并确保队列中的所有内容都是不同的。
- 我明白了,但在队列中检查不同的项是一个相等性的问题。你知道一个项是不同的方式是它不等于任何其他项。这些列表在放入队列后是否会更改?
感谢帮助我思考这个问题。不,一旦列表进入队列,它们就不会再更改。我相信我确实想要基于每个列表的内容创建一个哈希。
太棒了。我以前使用序列化做了一些复杂的事情,然后得到一个哈希。你的方法看起来更好。谢谢。
最近在研究在列表上创建哈希值的问题时,我发现了一个有趣的解决方案。在这个问题中,我发现了一个问题,以及一个修复这个问题的方法。
问题的出现是因为在现有的解决方案中,如果列表中有多个具有相同哈希码的项,那么可能会产生不准确的结果。这个问题可以通过以下示例来说明:
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` 应该为 `c` 和 `e` 产生相同的结果 - 它确实如此。到目前为止还不错。现在我们来尝试一下顺序不同的示例:
var f = new []{ "spam", "bar", "foo" };
糟糕了... `GetSequenceHashCode` 表示 `f` 等于 `c` 和 `e`,但事实并非如此。为什么会发生这种情况呢?让我们先来看看实际的哈希码值,以 `c` 为例:
int hashC = "foo".GetHashCode() ^ "bar".GetHashCode() ^ "spam".GetHashCode();
由于这里的确切数字并不重要,为了更清晰地演示,让我们假设这三个字符串的哈希码是 `foo=8`,`bar=16` 和 `spam=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" };
尽管 `a` 和 `b` 生成了不同的序列哈希,但 `GetSequenceHashCode` 表示 `a`、`c` 和 `d` 都是相同的。为什么呢?
如果你使用一个数与自身进行异或运算,你就会将它消除掉,即:
8 ^ 8 == 0; // 8 = 00001000 // ^ // 8 = 00001000 // = // 0 = 00000000
再次异或相同的数会得到原始结果,即:
8 ^ 8 ^ 8 == 8; // 8 = 00001000 // ^ // 8 = 00001000 // ^ // 8 = 00001000 // = // 8 = 00001000
因此,如果我们再次看一下 `a` 和 `c`,将简化后的哈希码代入:
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 // ↑ ↑ // 这两个彼此抵消掉了
对于 `d` 也是如此,其中每对 `foo` 和 `spam` 都会相互抵消。
在这个回答中,提供了一个很好的解决方案。将我的实现更改为 `IEnumerable` 的扩展,以包括其他集合。我很好奇种子和修饰符的值是从哪里来的,或者只要修饰符不是 0 或 1 就行。
31 和 487 都是质数。为什么使用质数?这个问题已经在其他回答中有了详细的解释,例如:[why should hash functions use a prime number modulus](https://stackoverflow.com/questions/1145217)