提问者:小点点

如何实现std::unordered_map


C++unordered_map冲突处理,调整大小和重新哈希

这是我之前提出的一个问题,我看到我对unordered_map是如何实现的有很多困惑。 我相信很多人和我一样感到困惑。 根据我在没有阅读标准的情况下所了解的信息:

每个unordered_map实现都在桶数组中存储到外部节点的链表。。。 不,对于大多数常见用途,这根本不是实现哈希映射的最有效方法。 不幸的是,unordered_map规范中的一个小“疏忽”几乎需要这种行为。 所需的行为是元素的迭代器在插入或删除其他元素时必须保持有效

我希望有人能解释一下这个实现,以及它如何符合C++标准定义(在性能要求方面),如果它真的不是实现散列映射数据结构的最有效的方法,那么如何改进它呢?


共1个答案

匿名用户

该标准有效地强制要求std::unordered_setstd::unordered_map实现使用开放散列,这意味着一个桶数组,每个桶包含逻辑(通常是实际的)列表的头部。 这个要求很微妙:这是默认最大加载因子为1.0的结果,并且保证表不会被重新散列,除非超出该加载因子:如果没有链接,这将是不切实际的,因为当加载因子接近1时,与封闭散列的冲突将变得不堪重负:

23.2.5/15:如果(n+n)(n+n)insertemplace成员不应影响迭代器的有效性; z*B,其中N是插入操作之前容器中的元素数,N是插入的元素数,B是容器的桶计数,z是容器的最大负载因子。

在23.5.4.2/1中构造函数的效果之一是:max_load_factor()返回1.0

(为了允许在不传递任何空桶的情况下进行最佳迭代,GCC的实现用迭代器将桶填充到一个包含所有值的单链接列表中:迭代器指向紧接在桶元素之前的元素,因此如果擦除桶的最后一个值,可以重新连接那里的下一个指针。)

关于你所引用的文字:

不,对于大多数常见用途,这根本不是实现哈希映射的最有效方法。 不幸的是,unordered_map规范中的一个小“疏忽”几乎需要这种行为。 所需的行为是元素的迭代器在插入或删除其他元素时必须保持有效

没有“失察”…… 所做的一切都是经过深思熟虑的,而且是在充分意识到的情况下完成的。 诚然,本可以达成其他妥协,但开放式哈希/链接方法是一种通用的合理妥协,它可以相当优雅地处理来自普通哈希函数的冲突,对于大小键/值类型不会太浪费,并且可以任意处理许多插入/擦除对,而不会像许多封闭式哈希实现那样逐渐降低性能。

从Matthew Austern的建议中可以看出:

我不知道在通用框架中有什么令人满意的开放寻址实现。 开放式寻址存在许多问题:

•有必要区分空缺职位和有人任职的职位。

•有必要将哈希表限制为具有默认构造函数的类型,并提前构造每个数组元素,或者维护一个数组,其中一些元素是对象,另一些元素是原始内存。

•开放寻址使冲突管理变得困难:如果您插入的元素的哈希代码映射到一个已经被占用的位置,那么您需要一个策略来告诉您下一步在哪里尝试。 这是一个已解决的问题,但已知的最佳解决方案都很复杂。

•当允许擦除元素时,冲突管理特别复杂。 (有关讨论,请参阅Knuth.) 标准库的容器类应该允许擦除。

•开放寻址的冲突管理方案倾向于采用固定大小的阵列,最多可容纳N个元素。 标准库的容器类应该能够在插入新元素时根据需要增长,直到可用内存的限制。

解决这些问题可能是一个有趣的研究项目,但是,在缺乏C++上下文中的实现经验的情况下,标准化一个开放寻址容器类是不合适的。

特别是对于数据小到可以直接存储在存储桶中的仅插入表,对于未使用的存储桶有一个方便的哨兵值,并且有一个好的散列函数,封闭散列方法可能会快一个数量级,并且使用的内存也会显著减少,但这不是通用的。

对散列表设计选项及其含义进行全面的比较和阐述对于S.O.来说是不合适的。 因为它太宽泛了,在这里无法恰当地处理。