最近关于C ++ unordered_map
的讨论使我意识到,由于查找的效率,我之前使用unordered_map
的大多数情况都应该使用map
(摊销O(1)与 O(log n))。大多数时候我使用地图我使用int
或std::strings
作为键,因此我对哈希函数的定义没有任何问题。我越是想到它,我就越发现在std::map
的简单类型的情况下我找不到使用unordered_map
的任何理由 - 我看了接口,并没有发现会影响我的代码的任何重大差异。
因此,问题是 - 在std::map
和unordered map
之类的简单类型的情况下,是否有任何真正的理由使用int
优于std::string
?
我从严格的编程角度问我 - 我知道它没有被完全认为是标准的,并且它可能会带来移植问题。
另外,我希望其中一个正确答案可能“它对于较小的数据集更有效”因为开销较小(是真的吗?) - 因此我想将问题限制在密钥数量不重要的情况下(> 1 024)。
编辑: 呃,我忘记了显而易见的事(感谢GMan!) - 是的,地图是当然有序的 - 我知道,我正在寻找其他原因。
答案 0 :(得分:360)
不要忘记map
保持他们的元素有序。如果你不能放弃,显然你不能使用unordered_map
。
需要记住的是unordered_map
通常使用更多内存。 map
只有几个管家指针,然后是每个对象的内存。相反,unordered_map
有一个大数组(在某些实现中可能会变得很大),然后为每个对象增加内存。如果你需要知道内存,map
应该更好,因为它缺少大数组。
所以,如果你需要纯查找检索,我会说unordered_map
是要走的路。但是总会有权衡,如果你负担不起,那么你就不能使用它。
仅从个人经验来看,在主要实体查找表中使用unordered_map
而不是map
时,我发现性能有了很大提高(当然是衡量的)。
另一方面,我发现重复插入和删除元素要慢得多。这对于一个相对静态的元素集合来说非常棒,但是如果你进行了大量的插入和删除操作,那么散列+分组似乎就会增加。 (注意,这是多次迭代。)
答案 1 :(得分:111)
如果您想比较std::map
和std::unordered_map
实施的速度,可以使用Google的sparsehash项目,该项目有一个time_hash_map程序来计时。例如,在x86_64 Linux系统上使用gcc 4.4.2
$ ./time_hash_map
TR1 UNORDERED_MAP (4 byte objects, 10000000 iterations):
map_grow 126.1 ns (27427396 hashes, 40000000 copies) 290.9 MB
map_predict/grow 67.4 ns (10000000 hashes, 40000000 copies) 232.8 MB
map_replace 22.3 ns (37427396 hashes, 40000000 copies)
map_fetch 16.3 ns (37427396 hashes, 40000000 copies)
map_fetch_empty 9.8 ns (10000000 hashes, 0 copies)
map_remove 49.1 ns (37427396 hashes, 40000000 copies)
map_toggle 86.1 ns (20000000 hashes, 40000000 copies)
STANDARD MAP (4 byte objects, 10000000 iterations):
map_grow 225.3 ns ( 0 hashes, 20000000 copies) 462.4 MB
map_predict/grow 225.1 ns ( 0 hashes, 20000000 copies) 462.6 MB
map_replace 151.2 ns ( 0 hashes, 20000000 copies)
map_fetch 156.0 ns ( 0 hashes, 20000000 copies)
map_fetch_empty 1.4 ns ( 0 hashes, 0 copies)
map_remove 141.0 ns ( 0 hashes, 20000000 copies)
map_toggle 67.3 ns ( 0 hashes, 20000000 copies)
答案 2 :(得分:75)
我回应GMan所做的大致相同的观点:根据使用的类型,std::map
可以(并且通常)比std::tr1::unordered_map
更快(使用VS 2008 SP1中包含的实现)
要记住一些复杂因素。例如,在std::map
中,您要比较键,这意味着您只需要查看键的开头,以区分树的右侧和左侧子分支。根据我的经验,几乎每次查看整个键都是因为你使用的是int,你可以在一条指令中进行比较。使用更典型的密钥类型(如std :: string),您通常只需比较几个字符。
std::map
可能会在unordered_map
甚至开始搜索之前完成搜索。
其次,虽然有几种调整哈希表大小的方法,但大多数方法都很慢 - 除非查找相当比插入和删除更频繁,否则std :: map将通常比std::unordered_map
快。
当然,正如我在上一个问题的评论中所提到的,你也可以使用树木表。这有利有弊。一方面,它将最坏的情况限制为树的情况。它还允许快速插入和删除,因为(至少在我完成它时)我使用了固定大小的表。消除所有表的大小调整,可以使哈希表更加简单,通常更快。
另一点:散列和基于树的地图的要求不同。散列显然需要散列函数和相等比较,其中有序映射需要小于比较。当然,我提到的混合动力需要两者。当然,对于使用字符串作为键的常见情况,这不是一个真正的问题,但某些类型的键比散列更适合排序(反之亦然)。
答案 3 :(得分:50)
我对@Jerry Coffin的回答很感兴趣,他建议有序的地图在经过一些实验(可以从pastebin下载)后表现出长串的性能提升,我发现这个对于随机字符串的集合似乎只适用于这种情况,当使用排序字典(包含具有大量前缀重叠的字词)初始化地图时,此规则会中断,可能是因为检索值所需的树深度增加。结果如下所示,第一个数字列是插入时间,第二个是获取时间。
g++ -g -O3 --std=c++0x -c -o stdtests.o stdtests.cpp
g++ -o stdtests stdtests.o
gmurphy@interloper:HashTests$ ./stdtests
# 1st number column is insert time, 2nd is fetch time
** Integer Keys **
unordered: 137 15
ordered: 168 81
** Random String Keys **
unordered: 55 50
ordered: 33 31
** Real Words Keys **
unordered: 278 76
ordered: 516 298
答案 4 :(得分:29)
我只想指出......有很多unordered_map
s。
在哈希映射上查找Wikipedia Article。根据使用的实施方式,查找,插入和删除方面的特征可能会有很大差异。
这对于STL增加unordered_map
最让我担心的是:他们必须选择一个特定的实现,因为我怀疑他们会走Policy
路,所以我们将被用于平均使用的实现,而对于其他情况则没有...
例如,一些哈希映射具有线性重新散列,而不是一次性重新散列整个哈希映射,而是在每次插入时重新分配一部分,这有助于分摊成本。
另一个例子:一些哈希映射使用一个简单的节点列表用于存储桶,其他使用一个映射,另一些不使用节点但找到最近的插槽,最后一些将使用节点列表但重新排序以便最后访问过的元素位于前面(就像缓存一样)。
所以目前我倾向于选择std::map
或者loki::AssocVector
(对于冻结的数据集)。
不要误解我的意思,我想在将来使用std::unordered_map
,但是当你想到所有的方式时,很难“信任”这种容器的可移植性。实施它以及由此产生的各种表现。
答案 5 :(得分:17)
map
使所有元素的迭代器保持稳定,在C ++ 17中,您甚至可以将元素从一个map
移动到另一个而不会使迭代器失效(如果没有任何可能的分配,则正确实现) map
时间通常更加一致,因为它们从不需要大量分配。unordered_map
实现的std::hash
,如果使用不受信任的输入(它使用带有常量种子的MurmurHash2),那么它很容易受到DoS的影响 - 请注意https://emboss.github.io/blog/2012/12/14/breaking-murmur-hash-flooding-dos-reloaded/ )。答案 6 :(得分:14)
散列表具有比常见映射实现更高的常量,这对于小容器而言变得很重要。最大尺寸是10,100,甚至可能是1,000或更多?常量与以前相同,但O(log n)接近O(k)。 (记住,对数复杂性仍然真的好。)
良好散列函数的作用取决于数据的特征;因此,如果我不打算查看自定义哈希函数(但后来肯定会改变我的想法,而且很容易因为我在所有内容附近输入),即使默认选择对许多数据源执行得体,我发现有序在这种情况下,我仍然默认映射而不是哈希表,这对于地图的本质来说是足够的帮助。
另外,你不必考虑为其他(通常是UDT)类型编写哈希函数,只需编写op< (无论如何你想要的。)
答案 7 :(得分:10)
我最近做了一个测试,它使50000合并和排序。这意味着如果字符串键相同,则合并字节字符串。并且应该对最终输出进行排序。所以这包括查找每次插入。
对于map
实现,完成作业需要200毫秒。对于unordered_map
+ map
,unordered_map
插入需要70 ms,map
插入需要80 ms。因此混合实现速度提高了50 ms。
在使用map
之前,我们应该三思而后行。如果您只需要在程序的最终结果中对数据进行排序,那么混合解决方案可能会更好。
答案 8 :(得分:10)
其他答案中给出了理由;这是另一个。
std :: map(平衡二叉树)操作分摊为O(log n),最差情况为O(log n)。 std :: unordered_map(哈希表)操作分摊为O(1),最差情况为O(n)。
这在实践中如何发挥作用是哈希表"打嗝"每隔一段时间进行O(n)操作,这可能是您的应用程序可以容忍的东西,也可能不是。如果它不能容忍它,你更喜欢std :: map over std :: unordered_map。
答案 9 :(得分:3)
摘要
假设排序不重要:
std::unordered_map
std::map
。这是因为对其的读取为O(log n)
。std::map
是个不错的选择。 std::unordered_map
。 历史背景
在大多数语言中,无序映射(也称为基于哈希的字典)是默认映射,但是在C ++中,您将有序映射作为默认映射。那是怎么发生的?有人错误地认为C ++委员会以其独特的智慧做出了这一决定,但不幸的是,事实比这更丑。
believed广泛存在于C ++中,默认情况下将有序映射作为默认值,因为关于如何实现它们没有太多参数。另一方面,基于哈希的实现还有很多事情要谈。因此,为了避免标准化中的僵局,他们just got along使用有序映射。在2005年左右,许多语言已经有了基于散列的实现的良好实现,因此委员会接受新的std::unordered_map
更容易。在一个理想的世界中,std::map
将是无序的,而我们将std::ordered_map
作为单独的类型。
性能
下面两个图应该可以说明问题(source):
答案 10 :(得分:0)
上述所有内容的小补充:
最好使用map
,当您需要按范围获取元素时,因为它们是经过排序的,因此可以将它们从一个边界迭代到另一个边界。
答案 11 :(得分:0)
如果您使用 Visual Studio 2010 编译项目 - 忘记 unordered_map 字符串。 如果您使用更现代的 Studio(如 2017 年) - 那么 unordered_map 比有序地图快得多。
答案 12 :(得分:-1)
来自:http://www.cplusplus.com/reference/map/map/
“在内部,地图中的元素总是按照其内部比较对象(比较类型)指示的特定严格弱排序标准按其键排序。
映射容器通常比unordered_map容器慢,可以通过键来访问各个元素,但它们允许根据子集的顺序直接迭代子集。“