我发现了Cuckoo Hash tables,他们看起来很不错
但是我发现的大多数示例代码都是使用2个表来实现的
这在我看来是错误的,因为2个表可能位于不同的内存页面中,并且我们有获取随机地址的开销,并且没有真正的位置。
是不是可以使用1个数组而不是2个?
是否可能无法检测元素何时被踢出2次并且是时候调整大小了?
答案 0 :(得分:2)
回答评论中的混淆:不,这不是语言特定的。如果您正在考虑内存局部性并希望确保两个表都关闭,那么单个分配是可行的方法(无论您如何分配)。在java中,这可能如下所示:
class TwoTables {
private static final int SIZE_TABLE_FIRST = 11, SIZE_TABLE_SECOND = 29;
public TwoTables() {
m_buffer = new int[SIZE_TABLE_FIRST + SIZE_TABLE_SECOND];
}
// consider similar setters...
public int getFirst(int key) {
return m_buffer[toIndex(hashFirst(key), SIZE_TABLE_FIRST, 0)];
}
public int getSecond(int key) {
return m_buffer[toIndex(hashSecond(key), SIZE_TABLE_SECOND, SIZE_TABLE_FIRST)];
}
private static int toIndex(int hash, int mod, int offset) {
return hash % mod + offset;
}
private static int hashFirst(int key) { return ...; }
private static int hashSecond(int key) { return ...; }
private final int[] m_buffer;
}
如果这比访问两个单独的数组更好地依赖于你的JVM:只需考虑JIT能够动态地将两个小分配合并为一个更大的分配 - 而不必执行任何索引魔法
答案 1 :(得分:2)
你绝对可以用一个哈希表做一个cuckoo哈希表;也就是说,每个对象的两个位置只是一个哈希表中的位置。
唯一需要解决的小问题是如何在布谷鸟驱逐循环中决定将两个位置中的哪一个用于被驱逐的钥匙。当然,如果第一个位置与实际位置相同,您可以尝试一个位置并使用另一个位置。应该可以使用SIMD并行计算两个哈希值,因此这个策略的成本可能很小。
但是,如果您想在cuckoo循环期间保证单个哈希计算,则有一个简单的解决方案:使用H0(k)
而不是使用H1(k)
和H0(k)
作为两个位置和H0(k) xor H1(k)
。 (如果H1
独立于H0
,则H0 xor H1
也是如此,因此xor不会影响哈希值的分布。)通过此修改,您始终可以找到“其他位置“k
通过用H1(k)
xor表示当前位置,因此循环中只需要一个哈希计算。
虽然这允许您使用单个哈希表,甚至可能简化代码,但是没有太多证据表明它改进了算法的操作。在我的有限测试中,它似乎将循环迭代次数增加了40-50%。 (虽然需要强调的是,在绝大多数情况下,可以在不进入循环的情况下将新密钥插入到表中,因此在实际执行时间内增加的循环次数几乎不可察觉。)
答案 2 :(得分:1)
嗯,所有形式的哈希都是在缓存上谋杀。
无论如何,您可以轻松地将两者合并为一张表。但是,你怎么知道你是在第一个哈希函数还是第二个哈希函数?选项是将元数据添加到每个存储桶中,或者通过运行第一个哈希函数计算出来,看看你是否获得了当前位置,并且只有当你在第一个时才运行第二个哈希函数。这要么需要额外的空间,要么运行更多的哈希函数。
将表拆分成2可以更有效地解决问题。统计上,无论表是否已拆分,您都需要相同数量的存储桶来存储相同数量的存储桶。所以你的整个哈希表变小了。
答案 3 :(得分:1)