无论顺序如何,获取字符串列表的哈希
Posted
技术标签:
【中文标题】无论顺序如何,获取字符串列表的哈希【英文标题】:Getting hash of a list of strings regardless of order 【发布时间】:2010-10-14 18:42:09 【问题描述】:我想写一个函数GetHashCodeOfList()
,它返回一个字符串列表的哈希码,不管顺序如何。给定 2 个具有相同字符串的列表应该返回相同的哈希码。
ArrayList list1 = new ArrayList()
list1.Add("String1");
list1.Add("String2");
list1.Add("String3");
ArrayList list2 = new ArrayList()
list2.Add("String3");
list2.Add("String2");
list2.Add("String1");
GetHashCodeOfList(list1) = GetHashCodeOfList(list2) //this should be equal.
我有几个想法:
我可以先对列表进行排序,然后将排序后的列表组合成1个长字符串,然后调用GetHashCode()
。但是排序是一个缓慢的操作。
我可以获取列表中每个单独字符串的哈希值(通过调用string.GetHashCode()
),然后将所有哈希值相乘并调用 Mod UInt32.MaxValue
。
例如:"String1".GetHashCode() * "String2".GetHashCode * … MOD UInt32.MaxValue
。但这会导致数字溢出。
有人有什么想法吗?
提前感谢您的帮助。
【问题讨论】:
在您的示例中,两个散列可能相等,因为您试图将 list2 的散列分配给 list1 的散列。 :P 【参考方案1】:这里有两种不同的方法,主要分为以下两个类别,就有效性和性能而言,每种方法通常都有自己的优点和缺点。最好为任何应用选择最简单的算法,并在必要时只在任何情况下使用更复杂的变体。
请注意,这些示例使用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) 发布。
【讨论】:
通过接受更差的分布,您可以使用 hash += element.GetHashCode() 来摆脱 (x,x) = (y,y)。 讽刺的是,我只是在想同样的事情。将编辑以提供替代方案。 对,所以现在我们的两个解决方案已经基本融合了。添加 unchecked 关键字来处理溢出(即在长列表的情况下)仍然是我认为的改进。如果你想添加,我还不如删除我的帖子...... 对,会的。还将制作这个社区 wiki,因为它确实是一个团队的努力:) 好的,太好了。我认为我们现在有一个很好的解决方案。 :)【参考方案2】:排序字符串列表的另一种方法是获取字符串的哈希码,然后对哈希码进行排序。 (比较整数比比较字符串更便宜。)然后您可以使用算法来合并哈希码,(希望)提供更好的分布。
例子:
GetHashCodeOfList<T>(IEnumerable<T> list)
List<int> codes = new List<int>();
foreach (T item in list)
codes.Add(item.GetHashCode());
codes.Sort();
int hash = 0;
foreach (int code in codes)
unchecked
hash *= 251; // multiply by a prime number
hash += code; // add next hash code
return hash;
【讨论】:
Guffa,您是否想将此添加到已接受的答案中,这将变成一个很棒的 wiki 答案?将这个想法融入其中会很好。 我不知道这里的确切用例是什么,但是如果这个操作必须执行多次,例如在添加之间,那么在添加新时直接计算哈希码可能很有用物品? 不会 GetHashCodeOfList(new int[0]) 和 GetHashCodeOfList(new int[]0) 都给出 0 的哈希码?因此 int hash=0 可能会更好地替换为 int hash = codes.Any()?7:0; @Brent:好点,但你可以从一个非零的值开始,以获得不同的结果。无论如何,让不同的列表返回相同的哈希码不是问题,除非您的列表都包含少于 32 位的数据,否则无法避免。【参考方案3】: Dim list1 As ArrayList = New ArrayList()
list1.Add("0")
list1.Add("String1")
list1.Add("String2")
list1.Add("String3")
list1.Add("abcdefghijklmnopqrstuvwxyz")
Dim list2 As ArrayList = New ArrayList()
list2.Add("0")
list2.Add("String3")
list2.Add("abcdefghijklmnopqrstuvwxyz")
list2.Add("String2")
list2.Add("String1")
If GetHashCodeOfList(list1) = GetHashCodeOfList(list2) Then
Stop
Else
Stop
End If
For x As Integer = list1.Count - 1 To 0 Step -1
list1.RemoveAt(list1.Count - 1)
list2.RemoveAt(list2.Count - 1)
Debug.WriteLine(GetHashCodeOfList(list1).ToString)
Debug.WriteLine(GetHashCodeOfList(list2).ToString)
If list1.Count = 2 Then Stop
Next
Private Function GetHashCodeOfList(ByVal aList As ArrayList) As UInt32
Const mask As UInt16 = 32767, hashPrime As Integer = Integer.MaxValue
Dim retval As UInt32
Dim ch() As Char = New Char()
For idx As Integer = 0 To aList.Count - 1
ch = DirectCast(aList(idx), String).ToCharArray
For idCH As Integer = 0 To ch.Length - 1
retval = (retval And mask) + (Convert.ToUInt16(ch(idCH)) And mask)
Next
Next
If retval > 0 Then retval = Convert.ToUInt32(hashPrime \ retval) 'Else ????
Return retval
End Function
【讨论】:
【参考方案4】:代码少了很多,但性能可能不如其他答案:
public static int GetOrderIndependentHashCode<T>(this IEnumerable<T> source)
=> source == null ? 0 : HashSet<T>.CreateSetComparer().GetHashCode(new HashSet<T>(source));
【讨论】:
HashSetEqualityComparer.GetHashCode
的实现实际上是一个异或,所以这个解决方案不仅效率低,而且产生平庸的哈希码。【参考方案5】:
这是一种混合方法。它结合了三种交换操作(异或、加法和乘法),将每一种操作应用于 32 位数的不同范围。每个操作的位范围都是可调的。
public static int GetOrderIndependentHashCode<T>(IEnumerable<T> source)
var comparer = EqualityComparer<T>.Default;
const int XOR_BITS = 10;
const int ADD_BITS = 11;
const int MUL_BITS = 11;
Debug.Assert(XOR_BITS + ADD_BITS + MUL_BITS == 32);
int xor_total = 0;
int add_total = 0;
int mul_total = 17;
unchecked
foreach (T element in source)
var hashcode = comparer.GetHashCode(element);
int xor_part = hashcode >> (32 - XOR_BITS);
int add_part = hashcode << XOR_BITS >> (32 - ADD_BITS);
int mul_part = hashcode << (32 - MUL_BITS) >> (32 - MUL_BITS);
xor_total = xor_total ^ xor_part;
add_total = add_total + add_part;
if (mul_part != 0) mul_total = mul_total * mul_part;
xor_total = xor_total % (1 << XOR_BITS); // Compact
add_total = add_total % (1 << ADD_BITS); // Compact
mul_total = mul_total - 17; // Subtract initial value
mul_total = mul_total % (1 << MUL_BITS); // Compact
int result = (xor_total << (32 - XOR_BITS)) + (add_total << XOR_BITS) + mul_total;
return result;
性能几乎与简单的 XOR 方法相同,因为每个元素对 GetHashCode
的调用支配了 CPU 需求。
【讨论】:
以上是关于无论顺序如何,获取字符串列表的哈希的主要内容,如果未能解决你的问题,请参考以下文章