std::unordered_map是如何实现的

How std::unordered_map is implemented

本文关键字:实现 何实现 unordered map std      更新时间:2023-10-16

c++无序映射冲突处理、调整大小和重新散列

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

每个无序映射实现都存储一个到外部的链表存储桶阵列中的节点。。。不,那根本不是最重要的实现最常见用途的哈希映射的有效方法。不幸的是undered_map几乎需要这种行为。所需的行为是元素的迭代器在插入或删除时必须保持有效其他元素

我希望有人能解释一下实现方式,以及它如何符合C++标准定义(就性能要求而言),如果它真的不是实现哈希图数据结构的最有效方法,那么如何改进它?

该标准有效地要求std::unordered_setstd::unordered_map-及其";多";Brothers-使用开放哈希,也就是单独链接。这意味着一个bucket数组,每个bucket都包含链表的头†。这个要求很微妙:这是的结果

  • 默认max_load_factor()为1.0(这意味着每当size()超过bucket_count()的1.0倍时,表将调整大小,以及
  • 保证表不会被重新散列,除非增长超过该负载因子

如果没有链接,这将是不切实际的,因为与哈希表实现的另一个主要类别的冲突——封闭哈希,也就是开放寻址——随着load_factor()](https://en.cppreference.com/w/cpp/container/unordered_map/load_factor)方法1。

参考文献:

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

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

†为了在不传递任何空bucket的情况下实现最佳迭代,GCC的实现将带有迭代器的bucket填充到一个包含所有值的单个单链列表中:迭代器直接指向该bucket元素之前的元素,因此如果擦除bucket的最后一个值,则可以重新连接那里的next指针。

关于您引用的文本:

不,对于大多数常见用途来说,这根本不是实现哈希图的最有效方法。不幸的是;监督;在unordered_map的规范中,几乎都需要这种行为。所需的行为是元素的迭代器在插入或删除其他元素时必须保持有效

不存在";"疏忽";。。。所做的一切都是经过深思熟虑的,而且是有充分意识的。的确,也可以达成其他妥协,但开放哈希/链接方法对于一般用途来说是一种合理的妥协,它可以相当优雅地处理来自平庸哈希函数的冲突,对于大小键/值类型来说不会太浪费,并且处理任意多个CCD_ 17/CCD_

作为意识的证据,来自Matthew Austern的提议:

我不知道在通用框架中有任何令人满意的开放寻址实现。开放寻址带来了许多问题:

•有必要区分空缺职位和被占用职位。

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

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

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

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

解决这些问题可能是一个有趣的研究项目,但是,由于缺乏C++环境下的实现经验,将开放寻址容器类标准化是不合适的。

特别是对于只插入数据小到可以直接存储在存储桶中的表、未使用存储桶的方便哨兵值和良好的哈希函数,封闭哈希方法可能会快大约一个数量级,使用的内存也会大大减少,但这不是通用的。

对S.O.来说,对哈希表设计选项及其含义的全面比较和阐述是不合适的,因为它太宽泛了,无法在这里正确解决。