【发布时间】:2011-03-28 04:40:39
【问题描述】:
有没有办法检测 Java Hash-map 中的冲突?任何人都可以指出一些可能发生大量碰撞的情况。当然,如果您覆盖对象的哈希码并简单地返回一个常量值,肯定会发生冲突。我不是在谈论那个。我想知道在前面提到的所有情况下会发生大量冲突无需修改默认哈希码实现。
【问题讨论】:
标签: java collections hash collision-detection
有没有办法检测 Java Hash-map 中的冲突?任何人都可以指出一些可能发生大量碰撞的情况。当然,如果您覆盖对象的哈希码并简单地返回一个常量值,肯定会发生冲突。我不是在谈论那个。我想知道在前面提到的所有情况下会发生大量冲突无需修改默认哈希码实现。
【问题讨论】:
标签: java collections hash collision-detection
简单示例:散列Long。显然有 64 位的输入和只有 32 位的输出。 Long 的哈希记录为:
(int)(this.longValue()^(this.longValue()>>>32))
即想象一下这是两个相邻的int 值,并对它们进行异或运算。
所以所有这些都将有一个 0 的哈希码:
0
1L | (1L << 32)
2L | (2L << 32)
3L | (3L << 32)
等
我不知道这是否算作“大量碰撞”,但这是容易制造碰撞的一个例子。
显然 任何 哈希值超过 232 个可能会发生冲突,但在许多情况下它们更难产生。例如,虽然我确实在 String 上看到了仅使用 ASCII 值的哈希冲突,但它们比上面的更难产生。
【讨论】:
我创建了一个项目来对这些事情进行基准测试:http://code.google.com/p/hashingbench/(用于具有链接、开放寻址和布隆过滤器的哈希表)。
除了 key 的 hashCode() 之外,您还需要知道 “smearing”(或“加扰”,我在该项目中称之为)函数的哈希表。来自this list,HashMap的smearing函数相当于:
public int scramble(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
因此,要在 HashMap 中发生冲突,必要和充分条件如下:scramble(k1.hashCode()) == scramble(k2.hashCode())。 如果 k1.hashCode() == k2.hashCode() 总是如此(否则,涂抹/加扰函数不会成为函数),所以这是一个足够,但不是发生碰撞的必要条件。
编辑:实际上,上述充分必要条件应该是compress(scramble(k1.hashCode())) == compress(scramble(k2.hashCode())) - compress 函数接受一个整数并将其映射到{0, ..., N-1},其中N 是数字桶,所以它基本上选择一个桶。通常,这只是简单地实现为hash % N,或者当哈希表大小是2 的幂(这实际上是具有2 的幂的哈希表大小的动机),如hash & N(更快)。 (“压缩”是 Goodrich 和 Tamassia 用来描述此步骤的名称,在 Data Structures and Algorithms in Java 中)。感谢 ILMTitan 发现我的马虎。
其他哈希表实现(ConcurrentHashMap、IdentityHashMap 等)有其他需求并使用另一种涂抹/加扰功能,因此您需要知道您在说哪一种。
(例如,HashMap 的涂抹功能被实施是因为人们将 HashMap 与具有最差类型 hashCode() 的对象一起用于没有涂抹的 HashMap 的旧的二表幂实现 - 对象不同用于选择存储桶的低位位很少或根本没有 - 例如new Integer(1 * 1024)、new Integer(2 * 1024) * 等。如您所见,HashMap 的涂抹功能尽其所能具有 所有位 影响低位)。
不过,所有这些都是为了在常见情况下正常工作 - 一个特殊情况是继承系统的 hashCode() 的对象。
PS:实际上,促使实现者插入 smearing 函数的绝对丑陋的情况是 Floats/Doubles 的 hashCode(),以及作为值的键的用法:1.0、2.0、3.0、4.0 ...,所有其中具有相同(零)的低位。这是相关的旧错误报告:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4669519
【讨论】:
scramble(k1.hashCode()) % c == scramble(k2.hashCode()) % c,其中c是容量,或者哈希表中的桶数?
其他两个答案我看到了一个很好的 IMO,但我只是想分享一下,测试您的 hashCode() 在 HashMap 中的表现如何的最佳方法是从您的类中实际生成大量对象,放它们在特定的 HashMap 实现中作为关键并测试 CPU 和内存负载。 1 或 200 万个条目是一个很好的衡量数字,但如果您使用预期的地图大小进行测试,您将获得最佳结果。
我刚刚查看了一个我怀疑其散列函数的类。所以我决定用该类型的随机对象填充 HashMap 并测试碰撞次数。我测试了正在调查的类的两个 hashCode() 实现。因此,我在 groovy 中编写了您在底部看到的扩展 HashMap 的 openjdk 实现的类,以计算 HashMap 中的冲突数(请参阅countCollidingEntries())。请注意,这些不是整个散列的真正冲突,而是包含条目的数组中的冲突。数组索引计算为hash & (length-1),这意味着这个数组的大小越短,你得到的冲突就越多。并且这个数组的大小取决于HashMap中的initialCapacity和loadFactor(当put()更多数据时它会增加)。
虽然最后我认为查看这些数字没有什么意义。 HashMap 使用错误的hashCode() 方法会更慢这一事实意味着,只需对 Map 中数据的插入和检索进行基准测试,您就可以有效地知道哪个 hashCode() 实现更好。
public class TestHashMap extends HashMap {
public TestHashMap(int size) {
super(size);
}
public TestHashMap() {
super();
}
public int countCollidingEntries() {
def fs = this.getClass().getSuperclass().getDeclaredFields();
def table;
def count =0 ;
for ( java.lang.reflect.Field field: fs ) {
if (field.getName() == "table") {
field.setAccessible(true);
table = field.get(super);
break;
}
}
for(Object e: table) {
if (e != null) {
while (e.next != null) {
count++
e = e.next;
}
}
}
return count;
}
}
【讨论】: