这里有两种不同的方法,主要分为以下两个类别,就有效性和性能而言,每种方法通常都有自己的优点和缺点。最好为任何应用选择最简单的算法,并在必要时只在任何情况下使用更复杂的变体。
请注意,这些示例使用EqualityComparer<T>.Default,因为这将干净地处理空元素。如果需要,您可以为 null 做得比零更好。如果 T 被限制为 struct 它也是不必要的。如果需要,您可以将 EqualityComparer<T>.Default 查找提升到函数之外。
交换运算
如果您对单个条目的哈希码(commutative)进行操作,那么无论顺序如何,这都会导致相同的最终结果。
数字有几个明显的选择:
异或
public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
foreach (T element in source)
{
hash = hash ^ EqualityComparer<T>.Default.GetHashCode(element);
}
return hash;
}
其中一个缺点是 { "x", "x" } 的哈希值与 { "y", "y" } 的哈希值相同。如果这对您的情况来说不是问题,那么它可能是最简单的解决方案。
加法
public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
foreach (T element in source)
{
hash = unchecked (hash +
EqualityComparer<T>.Default.GetHashCode(element));
}
return hash;
}
这里的溢出很好,因此显式 unchecked 上下文。
仍有一些令人讨厌的情况(例如 {1, -1} 和 {2, -2},但它更有可能没问题,特别是对于字符串。对于可能包含此类整数的列表,您可以总是实现一个自定义的散列函数(也许一个将特定值的重复索引作为参数并相应地返回一个唯一的散列码)。
这是一个以相当有效的方式解决上述问题的算法示例。它还具有大大增加生成的哈希码分布的好处(有关一些解释,请参阅最后链接的文章)。对该算法究竟如何产生“更好”的哈希码进行数学/统计分析将是相当先进的,但是在大范围的输入值上对其进行测试并绘制结果应该可以很好地验证它。
public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
int curHash;
int bitOffset = 0;
// Stores number of occurences so far of each value.
var valueCounts = new Dictionary<T, int>();
foreach (T element in source)
{
curHash = EqualityComparer<T>.Default.GetHashCode(element);
if (valueCounts.TryGetValue(element, out bitOffset))
valueCounts[element] = bitOffset + 1;
else
valueCounts.Add(element, bitOffset);
// The current hash code is shifted (with wrapping) one bit
// further left on each successive recurrence of a certain
// value to widen the distribution.
// 37 is an arbitrary low prime number that helps the
// algorithm to smooth out the distribution.
hash = unchecked(hash + ((curHash << bitOffset) |
(curHash >> (32 - bitOffset))) * 37);
}
return hash;
}
乘法
与加法相比,这几乎没有什么好处:小数以及正数和负数的混合它们可能会导致哈希位的更好分布。作为抵消这个“1”的负数,它变成了一个无用的条目,没有任何贡献,任何零元素都会导致零。
你可以特例零来避免这个重大缺陷。
public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 17;
foreach (T element in source)
{
int h = EqualityComparer<T>.Default.GetHashCode(element);
if (h != 0)
hash = unchecked (hash * h);
}
return hash;
}
先订购
另一种核心方法是先强制执行一些排序,然后使用您喜欢的任何哈希组合函数。排序本身是无关紧要的,只要它是一致的。
public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
{
int hash = 0;
foreach (T element in source.OrderBy(x => x, Comparer<T>.Default))
{
// f is any function/code you like returning int
hash = f(hash, element);
}
return hash;
}
这有一些显着的好处,因为f 中可能的组合操作可以具有明显更好的散列属性(例如位分布),但这会带来更高的成本。排序是O(n log n),集合的所需副本是内存分配,鉴于避免修改原始文件的愿望,您无法避免。 GetHashCode 实现通常应该完全避免分配。 f 的一种可能实现类似于加法部分下的最后一个示例中给出的实现(例如,左移任何恒定数量的位移后跟一个素数相乘 - 您甚至可以在每次迭代中使用连续素数,无需额外成本,因为它们只需要生成一次)。
也就是说,如果您正在处理可以计算和缓存哈希并在多次调用GetHashCode 时分摊成本的情况,这种方法可能会产生更好的行为。此外,后一种方法更加灵活,因为如果它知道元素的类型,它可以避免在元素上使用GetHashCode,而是对它们使用按字节操作来产生更好的散列分布。这种方法可能仅在性能被确定为重大瓶颈的情况下才有用。
最后,如果您想对哈希码主题及其有效性进行相当全面且相当非数学的概述,these blog posts 值得一读,尤其是实现简单的哈希算法(pt II) 发布。