如何为C++哈希表选择一个好的哈希函数?

34

我需要一个在C++中针对哈希表的高性能哈希函数实现。我已经搜索了一些,但只找到了一些询问“一般情况下什么是好的哈希函数”问题的答案。我考虑过CRC32(但从哪里找到好的实现?)以及一些加密算法。然而,我的表格具有非常特定的要求。

这就是表格的样子:

100,000 items max
200,000 capacity (so the load is 0.5)
hashing a 6-character string which is a part of English sentence
     examples: "become"    "and he"    ", not "

我的哈希表的首要优先事项是快速搜索(检索)。快速插入并不重要,但它会随着快速搜索而来。删除不重要,重新哈希也不是我要考虑的事情。为了处理冲突,我可能会使用分离链接法,如此处所述。我已经查看了这篇文章,但想听听那些曾处理过这种任务的人的意见。


我还添加了一个哈希函数作为另一个答案,你可能会喜欢。 - Robert Gould
如果你非常着急,为什么不在这个问题上设置悬赏奖励呢? - jmucchiello
重赏之下必有勇夫,如果没有人愿意提供有用的建议,我会设置悬赏,但我很惊喜 :) - DV.
无论如何,赏金的一个问题是你必须等待两天才能发布赏金。 - Robert Gould
22
你考虑过使用以下一种或多种通用哈希函数:http://www.partow.net/programming/hashfunctions/index.html,它们非常快速和高效。 - Matthieu N.
你确定哈希函数的性能至关重要吗?常用函数是将“unsigned int”旋转,比如3位,并加入下一个字节,然后对质数取模,这个在文本上运行良好。输入是否在潜在恶意方面(他们可能会提供给您100,000个字符串散列到少量桶中)有部分控制权?那么,您需要使其难以对付此类攻击的数据,例如通过“盐”(每个表都从秘密随机值开始)和一些加入的密码哈希(但对于短字符串可能不是很有效)。 - vonbrand
9个回答

24

现在假设您想要一个哈希,并且希望得到一些在您的情况下运行“非常快”的东西,因为您的字符串只有6个字符,所以您可以使用以下代码:

size_t precision = 2; //change the precision with this
size_t hash(const char* str)
{
   return (*(size_t*)str)>> precision;
}

CRC是慢吞吞的东西 ;)

说明: 这个方法通过将字符串指针的内容强制转换为"看起来像"size_t(基于硬件最佳匹配的int32或int64)。因此,字符串的内容被解释为一个原始数字,不再需要担心字符问题,然后将其按所需的精度进行位移(您可以调整此数字以获得最佳性能,我发现2适用于在几千个字符串中哈希字符串)。

另外一个非常棒的部分是,任何现代硬件上的良好编译器都会使用1条汇编指令对这样的字符串进行哈希,很难超越它;)


哇塞,太感谢了!我正在使用你在另一个答案中概述的哈希函数和二叉树来实现哈希表。 - DV.
是的,在你的代码中你正在除以2的因数,而在我的情况下我将数字除以4(2x2)。 - Robert Gould
14
请注意,这段代码在64位硬件上可能无法正常工作,因为转换将使用字符串的str[6]和str[7]部分,而它们并不属于该字符串。另外,在32位硬件上,您仅使用字符串中的前四个字符,因此可能会出现很多冲突。 - David Norman
2
我不认为这是一个好的算法。哈希输出增长非常线性,根本没有雪崩效应... - Unknown
“精度”完全是个名不符实的词……这个值用于丢弃一些字符中不太重要的位(具体哪些取决于字节序),从而简化哈希可能使用的不同值的数量。如果您使用std::uint32_tstd::uint16_t,您可以安全地访问数据的所有6个字节(在您的实现中,如果size_t是32位,则最后两个字符不会被哈希,如果是64位,则可能发生内存缓冲区读取溢出,导致崩溃),然后可能对它们进行异或运算,因为速度是优先考虑的。由于哈希函数较弱,在这里使用一个素数数量的桶会有所帮助。 - Tony Delroy
显示剩余5条评论

14

这个简单的多项式效果出奇地好。我是从微软研究院的Paul Larson那里学来的,他研究了各种哈希函数和哈希乘数。

unsigned hash(const char* s, unsigned salt)
{
    unsigned h = salt;
    while (*s)
        h = h * 101 + (unsigned) *s++;
    return h;
}

salt应该在创建哈希表之前被初始化为一些随机选择的值,以防止哈希表攻击。如果这对您来说不是问题,只需使用0。

表的大小也很重要,可最小化碰撞。听起来你的大小还可以。


2
如果你能确保你的字符串始终为6个字符,毫无例外,那么你可以尝试展开循环。 - Jackson
1
我认为 (unsigned char*) 应该改为 (unsigned char)。 - sgraham
我在循环中将转换类型的语句改为了(unsigned) - George V. Reilly
哈希表攻击链接现在已经失效了。它是否已经移动了? - Vincent Fourmond
谢谢,文森特。我已经更新了我的文章链接。我还更新了文章本身中的损坏链接。 - George V. Reilly
显示剩余2条评论

6

Boost.Functional/Hash 可能对您有用。我没有尝试过,因此无法保证其性能。

Boost 还有一个 CRC 库

我会首先看一下 Boost.Unordered(即 boost::unordered_map<>)。它使用哈希表而不是二叉树作为容器。

我相信一些 STL 实现在 stdext 命名空间中有一个 hash_map<> 容器。


4
由于您存储英文单词,大多数字符将是字母,并且在数据的最显著的两个位中不会有太多变化。除此之外,我建议保持非常简单,只使用异或运算。毕竟,您不是在寻找加密强度,而仅仅是为了合理均匀分布。大致可以按照以下方式进行:
size_t hash(const std::string &data) {
  size_t h(0);
  for (int i=0; i<data.length(); i++)
    h = (h << 6) ^ (h >> 26) ^ data[i];
  }
  return h;
}

除此之外,你是否看过std::tr1::hash作为哈希函数和/或std::tr1::unordered_map作为哈希表的实现?使用它们可能比实现自己的类节省很多工作。

谢谢你的建议!你能详细说明一下"h = (h << 6) ^ (h >> 26) ^ data[i];"是做什么的吗?就使用C++库而言,我将无法使用,因为这是一项课堂练习... - DV.
^ 是 C++ 中的异或运算符,<< 和 >> 则是左移和右移位运算符,用于在一定程度上“混合”数据。 - sth

4
您的表格大小将决定您应该使用什么大小的哈希。当然,您希望尽量减少冲突。我不确定您通过最大项和容量指定了什么(它们对我来说似乎是相同的),但无论如何,这两个数字都表明32位哈希足够了。您可能可以使用CRC16(约65,000种可能性),但您可能会遇到很多冲突。另一方面,解决冲突可能比CRC32哈希更快。
我建议您使用CRC32。您将找不到缺乏文档和示例代码的情况。由于您已经确定了最大值并且速度是优先考虑的因素,请使用指针数组。使用哈希生成索引。在冲突时,递增索引直到找到一个空桶...快速而简单。

2
如何简单化一些呢?
// Initialize hash lookup so that it maps the characters
// in your string to integers between 0 and 31
int hashLookup[256];

// Hash function for six character strings.
int hash(const char *str)
{
    int ret = 0, mult = 1;
    for (const char *p = str; *p; *p++, mult *= 32) {
        assert(*p >= 0 && *p < 256);
        ret += mult * hashLookup[*p];
    }

    return ret;
}

假设32位整数, 该算法每个字符使用5个比特位,因此哈希值只有30个比特位。也许你可以通过为前1或2个字符生成6个比特位来修复这个问题。如果你的字符集足够小,则可能不需要超过30个比特位。


2
如果您需要搜索短字符串且插入不是问题,也许您可以使用B树或2-3树,在您的情况下哈希并不能获得更多的优势。
您可以通过在每个节点中放置一个字母来实现此操作,因此首先检查节点“a”,然后检查“a”的子节点是否为“p”,再检查它的子节点是否为“p”,然后是“l”,最后是“e”。在存在“apple”和“apply”这样的情况下,您需要寻找到最后一个节点(因为唯一的区别在于最后一个“e”和“y”)。
但在大多数情况下,您只需几步就能获得单词(例如,“xylophone” => “x”->“ylophone”),因此您可以进行优化。这可能比哈希更快。

请详细说明如何使用6个字符的字符串作为键来创建B树?谢谢! - DV.
还有一件事,它将如何决定在“x”之后“ylophone”是唯一的子节点,因此需要以两个步骤检索它? - DV.
我的结构体是 { char* data; char link{'A', 'B', .., 'a', 'b', ' ', ..}; },并且它将测试根节点是否 (node->link['x'] != NULL) 以获取以“x”开头的可能单词。 - DV.
当你插入数据时,需要进行“排序”。了解堆和优先队列。 - Robert Gould

2
我的哈希表的首要目标是快速检索(检索)。
那么你使用的是正确的数据结构,因为在哈希表中搜索是O(1)! :)
CRC32应该做得很好。 实现并不那么复杂,主要基于XOR。 只需确保它使用良好的多项式即可。

1

不是这样的。它只将字符串转换为无符号长整型。试试看std::hash<int>(42)返回什么。别问我,为什么他们提供这种无用的功能。 - Jay-Pi
@Jay-Pi std::hash<int>(int)std::hash<string>(string) 是不同的函数。 - Raedwald
将std :: hash与stoul(l)进行比较,您会发现它们提供相同的内容。我不清楚为什么这在参考文献中没有记录。 因为该函数不是哈希函数,它只是创建“可哈希数据类型”。 然而,我不明白如何通过在模板后面隐藏它使其更容易使用。 - Jay-Pi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接