深入浅出 unordered_map 与 unordered_set——从使用到底层差异

前言

在 C++ 的标准模板库中,关联式容器一直是我们处理查找、去重、映射等问题的利器。我们早已熟悉 set 和 map 的有序特性,但当我们不那么关心顺序,只追求极致的插入、查找、删除效率时,unordered_set 和 unordered_map 便登场了。它们基于哈希表实现,平均时间复杂度 O(1),但相应的,它们对元素类型有特殊要求,迭代器也不再有序。本文会先介绍 unordered_set 的基本用法,再对比 set 的差异,接着引出 unordered_map,最后通过完整的性能测试代码让你直观感受"哈希"带来的速度提升。

一、unordered_set 系列入门

首先,我们从 unordered_set 开始。它的声明如下:

cpp 复制代码
template < class Key,
           class Hash = hash<Key>,
           class Pred = equal_to<Key>,
           class Alloc = allocator<Key> >
class unordered_set;
  • 第一个模板参数 Key 就是我们要存储的元素类型。

  • 第二个 Hash 是哈希函数,默认用 std::hash<Key>,它要求 Key 能够转换成整型值(以便计算桶下标)。如果默认不支持(比如自定义类),我们可以自己提供仿函数。

  • 第三个 Pred 是比较相等性的函数,默认是 std::equal_to<Key>,要求 Key 支持 operator==。

  • 第四个是空间配置器,通常我们用默认值即可。

    所以说,unordered_set 对 Key 的要求与 set 截然不同:set 要求支持 < 比较(用于红黑树排序),而 unordered_set 要求支持"转整形"和"=="。这种差异来自底层数据结构------哈希表。

与 set 的主要差异(三个维度)

  1. 迭代器类型:set 的迭代器是双向迭代器(可以 ++ 和 --),而 unordered_set 是单向迭代器(只能 ++)。

  2. 遍历顺序:set 底层是红黑树,中序遍历的结果是升序且去重的;unordered_set 底层是哈希桶,遍历结果是无序的,但也会去重。

  3. 性能:set 的增删查改复杂度为 O(log N),而 unordered_set 平均为 O(1)。在数据量较大时,哈希表通常更快。

让我们先用一个简单例子验证无序和遍历行为。

这里我们初始用列表插入了 2,3,5,6,7,2(注意重复的 2 会被去重),然后插入 45,最后从 begin 遍历到 end。由于底层是哈希表,输出的顺序不一定是输入顺序,例如可能是"7 5 2 3 6 45"之类的任意排列,但绝对不会出现重复值。这就是"无序 + 去重"的直观体现。

二、unordered_map 的基本使用

与unordered_set 类似,unordered_map 存储键值对(key-value)。它的模板参数多了一个 Mapped 类型,即值的类型。同样,它的 key 必须支持哈希和等于比较。它与 map 的差异与上面完全一致:

  • map 是有序(按 key 升序)双向迭代器,unordered_map 无序单向迭代器。

  • map 的 key 要求 <,unordered_map 要求哈希和 ==。

  • 性能上 unordered_map 通常更快。

函数演示插入和遍历:

这里我们向字典中插入了三个英文单词的中文释义,然后使用 C++17 的结构化绑定(auto& [k, v])遍历所有键值对。因为 unordered_map 无序,输出顺序可能不是 "insert"、"sort"、"test" 的顺序,而是随机的。但每个 key 都是唯一的,重复插入相同 key 会失败(除非使用 multiset 系列)。

三、深入性能对比------通过测试代码

为了切身感受 O(1) 与 O(log N) 的差距,我给出了一个完整的测试函数 test_set2。这个测试会生成大量随机数,分别插入 set 和 unordered_set,然后统计插入、查找、删除的耗时。

代码细节剖析:

  • 我们生成 N=100 万个随机数(加上 i 以减少重复,使得重复值相对少一些,更真实反映查询效率)。

  • 对于 unordered_set,我们调用了 reserve(N),这是提前把桶的数量扩展到足够容纳 N 个元素,避免插入过程中频繁 rehash(重新分配桶)带来的开销。这是使用 unordered 容器时一个重要的优化技巧。

  • 分别计时插入、查找、删除操作,并输出消耗的毫秒数(clock() 函数返回 CPU 时钟数,差值即为时间)。

  • 查找时统计成功找到的数量(应该等于容器的大小),用来验证数据完整性。

  • 最后打印两个容器的实际元素个数,它们应该相同(去重后)。

预期的结果

在大多数环境里,你会看到 unordered_set 的插入、查找、删除耗时都显著小于 set,通常差距在几倍到十几倍不等。这直接验证了 O(1) 对比 O(log N) 的优势。当然,如果数据量很小,或者重复值极多,差距可能不明显,但大规模数据下哈希表胜出。

四、关于 unordered_multimap 和 unordered_multiset

这两个容器与 multimap/multiset 类似,允许键重复。它们的接口和用法与普通版本一致,只是插入时不会去重,查找会返回所有匹配项。对于需要存储多个相同 key 的场景,可以直接选用它们。

五、总结

  • unordered_set / unordered_map 基于哈希桶,平均 O(1) 的效率,非常适合大规模查找去重。

  • 它们对 Key 的要求是支持哈希转换和相等比较,而不是小于比较。

  • 遍历顺序无序,迭代器为单向。

  • 性能明显优于 set/map,但会牺牲有序性和双向遍历的能力。

  • 在使用时,可主动 reserve 预分配桶数以提升插入效率。

相关推荐
倒头就睡的小比特4 天前
算法竞赛C++常用的STL
c++·算法
weilx12344 天前
C++笔记-文件IO-<fcntl.h>
c++
Smileyqp沛沛4 天前
前端?C++ ?较大差异基础罗列
c++·基础·前端转c++
C语言小火车4 天前
C/C++ 为什么需要编译器?
开发语言·c++
旖旎夜光4 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
吞下星星的少年·-·4 天前
C++ 萌新语法入门篇
c++·算法比赛
霍霍的袁4 天前
【C++】map 和 set 的使用 | 从用法到底层
开发语言·c++·学习·visual studio
another heaven4 天前
【算法/C++ MD5算法能否逆解码?原理、C++实现与同类哈希算法对比】
c++·算法·哈希算法
程序猿编码4 天前
告别改源码适配模型:纯 C++ 可配置 LLM 推理引擎,全格式全结构兼容
c++·大模型·llm·推理引擎
此生决int4 天前
深入理解C++系列(20)——C++11(下)
开发语言·c++