在学习数据结构,特别是哈希表时,我们被告知为数据类型发明有效的哈希函数是一项非常艰巨的任务,但有人建议存在一个快速的快捷方式。也就是说,如果我们可以假设对象不在内存中移动,并且我们可以将对象相等定义为具有相同的内存地址(使用引用相等而不是值相等),那么我们可以获得如下对象的哈希码:
#include<iostream>
template<typename T>
class hashcoder {
private:
union impl {
T* ptr;
int32_t hashcode; // or int64_t on a 64-bit architecture = architecture with 64-bit pointers
};
public:
static int32_t hash(const T& r) {
impl i;
i.ptr = &r;
return i.hashcode;
}
};
class myclass {
// whatever
};
int main() {
myclass m;
std::cout << hashcoder<myclass>::hash(m) << std::endl;
}
所以我的问题是:
答案 0 :(得分:4)
将指针投射到uintptr_t
。不需要工会。此外,uintptr_t
具有适合平台的大小,因此您不再需要使用int32_t
等等。
uintptr_t hash(const T &r)
{
return uintptr_t(&r);
}
(如果散列必须是32位,则将其转换为uint32_t
,或者在64位平台上,使用appropriate magic合并两半。)
答案 1 :(得分:2)
首先,您的代码假定T
的不同实例始终不同(对于!=
运算符)。情况并非总是如此,并且对于例如std::string
当你想要对字符串值(即内容)进行散列时,而不是在某个地址上进行散列....同样,如果你想散列(数学)整数向量,你应该对它们的内容进行散列(数学)矢量的组成部分。)
然后你应该知道只使用某些东西的地址作为它的散列可能是次优的,因为足够大的对象的地址往往是8或16字节的倍数(恰好,该对象类型的对齐),并且通常在同一时刻分配的对象的地址非常相似。实际上,指针的某些“中间位”可能比低位或非常高位更“随机”。
稍微好一点的方法可能是对指针地址进行一些按位算术,例如
static inline int32_t ptrhash(const void*p) {
uintptr_t u = (uintptr_t)p;
return u ^ (u >> 10);
}
或
static inline int32_t ptrhash(const void*p) {
uintptr_t u = (uintptr_t)p;
return (u * 200771) + (u % 300823);
}
200771和300823都是素数。您可以使用按位xor +
^
或任何技巧正确混合地址的位。
当然,YMMV并且绝对是系统特定的(例如,取决于您的系统是否有ASLR)
对于某些class T
,另一种方法可能是在构造函数中生成-in,例如在T实例的构造时,一些随机散列(使用快速 PRNG,如lrand48 ...),并将该散列作为私有实例成员变量。或者只是使用一些静态计数器,为每个实例唯一编号,并将该数字用作哈希值。
重要的是要确保所有实例都不同!如果你想对内容进行哈希处理,情况并非如此(但请参阅memoization等等。)。
另外,我不同意你的老师:确实,发明一个非常好的哈希函数很难,但是制作一个“足够好”的平均散列函数通常很容易(例如使用与素数的线性组合组成部分的哈希系数,见Bezout's theorem)。而通常,在 practice 中,这种“简单”的哈希函数运行得很好(当然也有例外情况,不可能的最坏情况可能很糟糕)。
另请阅读perfect hash,例如使用GNU gperf。
答案 2 :(得分:0)
如果你想要引用相等,那么使用地址作为哈希码没有任何问题。但是,实现它比工会更实用的方法是使用intptr_t
。如果intptr_t
大于int32_t
,您可以与-1一起使用,然后static_cast
添加到uint32_t
。
答案 3 :(得分:0)
您只需要一个功能模板。 以下是您可以做的(假设您的哈希表具有有限数量的存储桶):
template <typename T>
uintptr_t hashcode(const T &obj, size_t size) {
return reinterpret_cast<uintptr_t>(&obj) % size;
}