CPP学习笔记—unordered_map
1. unordered_map 是什么?
std::unordered_map 是 C++ 标准模板库(STL)中的一个关联容器,它存储了由“键(Key)”和“值(Value)”组成的元素对(std::pair<const Key, Value>)。它的核心特点是:
- 无序性:元素在容器内的存储顺序是无序的,遍历
unordered_map时,得到的元素顺序与插入顺序无关,并且在不同编译环境下可能不同。 - 基于哈希表:其内部实现是一个哈希表(Hash Table)。
- 高效查询:由于使用了哈希表,它提供了非常快的平均时间复杂度的操作:
- 插入、删除、查找:平均 O(1)
- 最坏情况:O(N),其中 N 是容器中元素的数量(当发生严重的哈希冲突时)。
核心概念:哈希表
为了理解 unordered_map,需要了解哈希表的工作原理:
- 哈希函数 (Hash Function):当插入一个键值对时,
unordered_map会使用一个哈希函数将“键”转换成一个整数,这个整数被称为哈希值(Hash Code)。 - 桶 (Bucket):
unordered_map内部维护一个数组,这个数组被称为“桶”。哈希值经过处理后会映射到其中一个桶的索引上。 - 存储:该键值对就被存放在这个索引对应的桶中。
- 哈希冲突 (Collision):如果两个不同的键经过哈希函数计算后得到了相同的桶索引,就发生了“哈希冲突”。
unordered_map通常使用拉链法 (Chaining) 解决冲突,即在每个桶中维护一个链表(或其他数据结构),所有映射到该桶的元素都存储在这个链表中。
与 std::map 的关键区别
| 特性 | std::unordered_map |
std::map |
|---|---|---|
| 底层实现 | 哈希表 (Hash Table) | 红黑树 (Red-Black Tree) |
| 元素顺序 | 无序 | 按键自动排序 (升序) |
| 查找复杂度 | 平均 O(1),最坏 O(N) | 严格 O(log N) |
| 插入/删除复杂度 | 平均 O(1),最坏 O(N) | 严格 O(log N) |
| 对键的要求 | 必须可哈希 (提供 std::hash) 且可比较相等 (operator==) |
必须可比较大小 (operator<) |
| 内存开销 | 通常更高,因为需要维护桶数组 | 相对较低 |
| 适用场景 | 追求极致的查找、插入、删除性能,且不关心元素顺序 | 需要按键排序或进行范围查找 (如查找所有大于某个值的键) |
2. 何时使用 unordered_map?
优先选择 unordered_map,除非有以下需求:
- 需要对键进行排序。
- 需要根据键进行范围查找(例如,找到所有在 ‘a’ 和 ‘f’ 之间的键)。
- 性能瓶颈在于最坏情况,无法接受偶尔 O(N) 的操作延迟(例如在硬实时系统中)。
在绝大多数场景下,unordered_map 的平均 O(1) 性能使其成为构建字典、缓存、频率计数等功能的首选。
3. 基本用法
包含头文件和声明
1 |
|
插入元素
有多种插入方式:
1. 使用 operator[]
这是最简单直观的方式。如果键已存在,则更新其值;如果不存在,则创建新元素并进行值初始化(对于 int 就是 0),然后再赋新值。
1 | word_counts["apple"] = 5; // 插入或更新 |
2. 使用 insert()insert() 更为灵活,它返回一个 std::pair<iterator, bool>。bool 值为 true 表示插入成功(键之前不存在),为 false 表示插入失败(键已存在)。
1 | // 使用 std::make_pair |
3. 使用 emplace() (C++11, 推荐)emplace() 可以就地构造元素,避免了创建临时对象,效率更高。
1 | // 直接在容器内用 "grape" 和 7 构造键值对 |
访问元素
1. 使用 operator[]
简单直接,但有风险:如果键不存在,它会自动插入一个默认构造的值!这可能不是想要的行为。
1 | std::cout << "Apples: " << word_counts["apple"] << std::endl; // OK |
2. 使用 at()
更安全的方式。如果键存在,返回其值的引用;如果不存在,会抛出 std::out_of_range 异常。
1 | try { |
3. 使用 find() (最常用)
这是最标准的“检查并访问”模式。find() 返回一个指向元素的迭代器,如果键不存在,则返回 end() 迭代器。
1 | auto it = word_counts.find("apple"); |
删除元素
1. 按键删除 erase(key)
返回被删除元素的数量(对于 unordered_map 总是 0 或 1)。
1 | size_t num_erased = word_counts.erase("banana"); |
2. 按迭代器删除 erase(iterator)
如果已经有了一个有效的迭代器(例如从 find() 返回的),用它来删除效率更高。
1 | auto it_grape = word_counts.find("grape"); |
检查存在性
1. find() != end() (通用)
这是最传统、最通用的方法。
1 | if (word_counts.find("orange") != word_counts.end()) { |
2. count(key)
由于 unordered_map 的键是唯一的,count() 只会返回 0 或 1,可以直观地用于判断。
1 | if (word_counts.count("orange")) { // count("orange") 返回 1,等效于 true |
3. contains(key) (C++20)
C++20 引入了 contains() 成员函数,代码更简洁、意图更明确。
1 | // 如果编译器支持 C++20 |
遍历
1. 使用 C++11 范围 for 循环 (推荐)
1 | std::cout << "\n--- Current Map Contents ---" << std::endl; |
2. 使用迭代器
1 | for (auto it = word_counts.begin(); it != word_counts.end(); ++it) { |
获取大小和清空
1 | std::cout << "Map size: " << word_counts.size() << std::endl; |
4. 高级主题:自定义类型作为键 (Key)
如果想用自定义的 struct 或 class 作为键,必须告诉 unordered_map 两件事:
- 如何判断两个键相等? -> 通过重载
operator==。 - 如何为键生成哈希值? -> 通过特化
std::hash模板。
示例:使用 struct Student 作为键
1 |
|
关键点:当 find 或 insert 一个 Student 对象时,unordered_map 首先调用 std::hash<Student> 计算其哈希值,找到对应的桶。如果桶中有多个元素(发生冲突),它会再使用 operator== 来精确匹配提供的 Student 对象。
5. 性能考量与调优
负载因子 (Load Factor)
- 定义:
负载因子 = 容器中的元素数 / 桶数(size() / bucket_count())。 - 意义:它衡量了哈希表的“拥挤”程度。负载因子越高,哈希冲突的概率越大,性能越可能下降。
- 自动调整:当负载因子超过一个阈值(默认为 1.0),
unordered_map会自动进行重哈希 (rehash):它会创建一个更大的桶数组,并把所有现有元素重新计算哈希值并放入新桶中。这是一个非常耗时的 O(N) 操作。
可以通过以下函数来管理它:
load_factor(): 获取当前负载因子。max_load_factor(): 获取或设置触发重哈希的负载因子阈值。
1 | // 将阈值设为 0.75,减少冲突,但会更频繁地重哈希 |
哈希冲突 (Hash Collision)
选择一个好的哈希函数至关重要。一个好的哈希函数应该让键尽可能均匀地分布到所有桶中,以减少冲突。对于自定义类型,组合哈希值时要避免简单的加法,因为像 {a, b} 和 {b, a} 这样的组合会产生相同的哈希值。使用位移和异或是更稳妥的方式。
桶 (Bucket) 接口
unordered_map 提供了一些接口用来观察其内部状态:
bucket_count(): 获取桶的总数。bucket_size(n): 获取第n个桶中的元素数量。bucket(key): 获取某个键所在的桶的索引。
这些接口主要用于调试和性能分析。
预分配空间 (reserve)
如果预先知道大概要存储多少个元素,使用 reserve() 可以避免多次耗时的自动重哈希过程。
1 | // 假设我们知道要存储约 1000 个单词 |
6. 完整的综合示例
任务:统计一篇文章中每个单词出现的次数。
1 |
|
输出:
1 | Word Frequency Report: |









