[英]How std::unordered_map is implemented
c ++ unordered_map 碰撞处理,调整大小和重新散列
这是我之前提出的一个问题,我已经看到我对 unordered_map 的实现方式有很多困惑。 我相信很多其他人跟我一样有这种困惑。 根据我在没有阅读标准的情况下所知道的信息:
每个 unordered_map 实现都将一个链表存储到桶数组中的外部节点......不,这根本不是实现最常见用途的哈希映射的最有效方法。 不幸的是, unordered_map 规范中的一个小“疏忽”几乎需要这种行为。 所需的行为是元素的迭代器在插入或删除其他元素时必须保持有效
我希望有人可以解释实现以及它如何符合 C++ 标准定义(在性能要求方面),如果它真的不是实现哈希映射数据结构的最有效方法,如何改进它?
该标准有效地要求std::unordered_set
和std::unordered_map
- 以及它们的“多”兄弟 - 使用 开放散列又名单独链接,这意味着一个桶数组,每个桶都持有链表的头部†。 这个要求很微妙:它是以下结果的结果:
max_load_factor()
为 1.0(这意味着只要size()
超过bucket_count()
1.0 倍,表就会调整size()
,并且 如果没有链接,这将是不切实际的,因为与哈希表实现的其他主要类别(封闭哈希,即开放寻址load_factor()
的load_factor()
变得势不可挡,因为load_factor()
]( https://en.cppreference.com/w/cpp/container /unordered_map/load_factor ) 接近 1。
参考:
23.2.5/15:如果
(N+n) < z * B
,则insert
和emplace
成员不应影响迭代器的有效性,其中N
是插入操作之前容器中元素的数量,n
是插入的元素,B
是容器的桶数,z
是容器的最大负载因子。在 23.5.4.2/1 的构造函数的效果中:
max_load_factor()
返回1.0
。
† 为了在不传递任何空桶的情况下允许最佳迭代,GCC 的实现将带有迭代器的桶填充到一个包含所有值的单链表中:迭代器指向该桶元素之前的元素,因此next
指针可以是如果擦除存储桶的最后一个值,则重新连接。
关于你引用的文字:
不,这根本不是为大多数常见用途实现哈希映射的最有效方法。 不幸的是, unordered_map 规范中的一个小“疏忽”几乎需要这种行为。 所需的行为是元素的迭代器在插入或删除其他元素时必须保持有效
没有“监督”......所做的事情是非常深思熟虑的,并且是在充分意识到的情况下完成的。 的确,其他的妥协可能已经被击中,但开放散列/链接方法对于一般用途来说是一个合理的妥协,它可以合理优雅地处理来自平庸散列函数的冲突,对于小的或大的键/值类型不会太浪费,并处理任意多的insert
/ erase
对,而不会像许多封闭散列实现那样逐渐降低性能。
作为意识的证据,来自Matthew Austern 的提议:
我不知道在通用框架中任何令人满意的开放寻址实现。 开放寻址存在许多问题:
• 有必要区分空缺职位和被占用职位。
• 要么将哈希表限制为具有默认构造函数的类型,并提前构造每个数组元素,要么维护一个数组,其中一些元素是对象,而另一些元素是原始内存。
• 开放寻址使冲突管理变得困难:如果您要插入一个哈希码映射到已占用位置的元素,您需要一个策略来告诉您下一步尝试的位置。 这是一个已解决的问题,但最著名的解决方案是复杂的。
• 当允许擦除元素时,冲突管理尤其复杂。 (有关讨论,请参阅 Knuth。)标准库的容器类应该允许擦除。
• 开放寻址的冲突管理方案倾向于假设一个固定大小的数组,最多可容纳 N 个元素。 标准库的容器类应该能够在插入新元素时根据需要增长,直至达到可用内存的限制。
解决这些问题可能是一个有趣的研究项目,但是,在缺乏 C++ 上下文中的实现经验的情况下,将开放寻址容器类标准化是不合适的。
特别是对于数据小到可以直接存储在桶中的仅插入表、未使用桶的方便标记值和良好的散列函数,封闭散列方法可能大约快一个数量级并使用更少的内存,但是这不是通用目的。
对哈希表设计选项及其含义的完整比较和阐述对于 SO 来说是题外话,因为它太宽泛而无法在此处正确解决。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.