深入浅出 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 预分配桶数以提升插入效率。

相关推荐
C++ 老炮儿的技术栈4 小时前
std::atomic 是什么
linux·c语言·网络·c++·visual studio
All for pursuit.5 小时前
【完结】LeetCodeHot100题 详细解题过程及代码汇总
数据结构·c++·算法·leetcode
重生之小比特5 小时前
【算法拾光】双指针|移动零 + 复写零
数据结构·c++·算法
多弗朗皮卡丘6 小时前
C++异常
开发语言·c++·异常
米糕闯编程6 小时前
鱼香ros2(十七)C++小海龟巡逻
开发语言·c++·机器人·ros2·服务·小海龟
栈帧之外7 小时前
【C++】红黑树实现
开发语言·c++
程序员老陆7 小时前
C++中同一个回调函数被多个设备并发调用:并发问题不在“函数”,而在“状态”
开发语言·c++·程序设计
辛苦才能7 小时前
C++ unordered_map/set 深度解析:快在O(1),输在无序,与map/set怎么选
c++·哈希算法·散列表
sbjdhjd7 小时前
从 preg_replace 规则到缓存绕过:一次授权 PHP 防护实验中的图片载荷、回调检测与哈希碰撞复盘
网络安全·数据挖掘·php·文件包含·宝塔·哈希·火绒
垆边人似月.9 小时前
找出最小的 K 个数
数据结构·c++·算法