1. 基本概念
std::unordered_map 是基于哈希表的键值容器。
cpp
#include <unordered_map>
特性:
- 每个键唯一;
- 键关联一个值;
- 元素不保证顺序;
- 平均查找、插入、删除复杂度为 O(1);
- 最坏情况下可能退化为 O(n)。
2. 基本用法
cpp
#include <iostream>
#include <string>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> scores;
scores["Alice"] = 90;
scores.emplace("Bob", 85);
scores.insert({"Carol", 95});
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << '\n';
}
}
遍历顺序不固定。
3. 常用接口
cpp
map.empty();
map.size();
map.clear();
map.insert({key, value});
map.emplace(key, value);
map.try_emplace(key, constructor_args...);
map.insert_or_assign(key, value);
map.find(key);
map.contains(key); // C++20
map.count(key);
map.erase(key);
map.erase(iterator);
map[key];
map.at(key);
4. operator[] 与 at() 的区别
operator[]
cpp
std::unordered_map<std::string, int> counts;
int value = counts["unknown"];
若键不存在,会插入:
cpp
{"unknown", 0}
因此 operator[] 不适合纯查询场景。
at()
cpp
int value = counts.at("camera");
若键不存在,抛出 std::out_of_range。
find()
cpp
auto it = counts.find("camera");
if (it != counts.end()) {
int value = it->second;
}
只查询时通常推荐 find() 或 C++20 contains()。
5. 插入接口区别
5.1 insert
键存在时不覆盖。
cpp
auto [it, inserted] = map.insert({"camera", 1});
5.2 emplace
原地构造键值对。
cpp
map.emplace("camera", 1);
键已存在时,参数仍可能已经参与临时对象构造。
5.3 try_emplace
键存在时,不构造值对象。
cpp
map.try_emplace("camera", expensive_constructor_arg);
当值对象构造昂贵时更合适。
5.4 insert_or_assign
键不存在则插入,存在则覆盖值。
cpp
map.insert_or_assign("camera", 2);
5.5 operator[]
适合计数和确保键存在。
cpp
++word_count[word];
6. 典型计数场景
cpp
#include <string>
#include <unordered_map>
#include <vector>
std::unordered_map<std::string, int> countWords(
const std::vector<std::string>& words
) {
std::unordered_map<std::string, int> counts;
counts.reserve(words.size());
for (const auto& word : words) {
++counts[word];
}
return counts;
}
7. 哈希表基础
unordered_map 通常包含:
- 桶数组;
- 根据哈希值分配到桶中的节点;
- 桶内冲突处理结构。
查找过程通常是:
- 计算键的哈希值;
- 根据桶数量定位桶;
- 在桶内使用相等比较器寻找目标键。
8. 装载因子与扩容
cpp
std::unordered_map<int, std::string> devices;
devices.reserve(1000);
devices.max_load_factor(0.75f);
常用状态:
cpp
devices.bucket_count();
devices.load_factor();
devices.max_load_factor();
装载因子定义:
text
load_factor = size / bucket_count
当元素数量增大到一定程度,容器会 rehash:
- 增加桶数量;
- 重新分配所有元素到新桶;
- 使迭代器失效;
- 产生一次较明显的性能开销。
提前调用 reserve() 可以减少扩容次数。
9. 自定义键类型
cpp
#include <functional>
#include <string>
#include <unordered_map>
struct SensorKey {
int device_id;
int channel;
bool operator==(const SensorKey& other) const {
return device_id == other.device_id &&
channel == other.channel;
}
};
struct SensorKeyHash {
std::size_t operator()(const SensorKey& key) const {
std::size_t h1 = std::hash<int>{}(key.device_id);
std::size_t h2 = std::hash<int>{}(key.channel);
return h1 ^ (h2 << 1);
}
};
std::unordered_map<SensorKey, std::string, SensorKeyHash> sensors;
必须满足:
text
若 key1 == key2,则 hash(key1) == hash(key2)
10. 组合哈希函数
简单组合:
cpp
std::size_t combineHash(std::size_t seed, std::size_t value) {
seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2);
return seed;
}
使用:
cpp
struct PointHash {
std::size_t operator()(const Point& p) const {
std::size_t seed = 0;
seed = combineHash(seed, std::hash<int>{}(p.x));
seed = combineHash(seed, std::hash<int>{}(p.y));
return seed;
}
};
哈希组合需要兼顾:
- 分布均匀;
- 计算速度;
- 与相等规则一致;
- 对输入模式不敏感。
11. 值类型为复杂对象
cpp
struct DeviceState {
int status;
std::string message;
};
std::unordered_map<int, DeviceState> states;
states.try_emplace(
1001,
DeviceState{1, "online"}
);
通过结构化绑定遍历:
cpp
for (auto& [id, state] : states) {
state.status = 0;
}
只读遍历:
cpp
for (const auto& [id, state] : states) {
}
12. 值类型为智能指针
cpp
#include <memory>
std::unordered_map<int, std::unique_ptr<Device>> devices;
devices.emplace(
1,
std::make_unique<Device>()
);
这可以清晰表达:
- 容器拥有对象;
- 元素删除时对象自动析构;
- 避免手动
delete。
13. 迭代器失效规则
插入
- 未触发 rehash 时,已有迭代器通常保持有效;
- 触发 rehash 后,所有迭代器失效。
删除
- 指向被删除元素的迭代器、指针、引用失效;
- 其他元素通常不受影响。
reserve 和 rehash
可能使所有迭代器失效。
不要在遍历时无条件插入大量新键,除非已提前 reserve() 或正确处理迭代器失效。
14. 遍历中安全删除
cpp
for (auto it = map.begin(); it != map.end(); ) {
if (shouldErase(it->first, it->second)) {
it = map.erase(it);
} else {
++it;
}
}
C++20:
cpp
std::erase_if(map, [](const auto& item) {
return item.second.expired();
});
15. 与 map 的区别
| 对比项 | unordered_map |
map |
|---|---|---|
| 底层结构 | 哈希表 | 平衡搜索树 |
| 元素顺序 | 无序 | 按键有序 |
| 平均查找 | O(1) | O(log n) |
| 最坏查找 | O(n) | O(log n) |
| 范围查询 | 不支持 | 支持 |
| 哈希函数 | 需要 | 不需要 |
| 比较器 | 相等比较 | 顺序比较 |
| 迭代顺序 | 不稳定 | 稳定有序 |
| 内存特点 | 桶数组和节点 | 树节点指针 |
16. 常见应用
- 字符串词频统计;
- ID 到对象映射;
- 缓存;
- 配置项查询;
- 状态表;
- 图的邻接表;
- 去重并附加属性;
- 消息类型到处理函数映射;
- 稀疏数据索引。
处理函数映射:
cpp
#include <functional>
#include <unordered_map>
using Handler = std::function<void()>;
std::unordered_map<int, Handler> handlers;
handlers.emplace(1, [] {
// process type 1
});
17. 性能优化建议
- 已知规模时调用
reserve()。 - 设计分布良好的哈希函数。
- 避免在热点路径反复构造临时字符串键。
- 对字符串可考虑异构查找。
- 复杂值类型优先使用
try_emplace()。 - 更新已有值可使用
insert_or_assign()。 - 不依赖遍历顺序。
- 对强实时、极端最坏复杂度敏感的场景,评估
map或专用哈希容器。 - 小数据集先进行基准测试,
vector线性查找可能更快。 - 不要通过过低的
max_load_factor盲目换取速度,以免内存显著增加。
18. 常见错误
18.1 查询时意外插入
cpp
if (map[key] == 1) {
}
若键不存在,会创建新元素。
推荐:
cpp
auto it = map.find(key);
if (it != map.end() && it->second == 1) {
}
18.2 修改键
键在容器中视为 const,不能直接修改,否则会破坏哈希结构。
需要修改键时:
- 删除旧键;
- 插入新键;
- 或使用 C++17 节点句柄。
cpp
auto node = map.extract(old_key);
if (!node.empty()) {
node.key() = new_key;
map.insert(std::move(node));
}
18.3 假设哈希冲突不会发生
哈希值相同不代表键相等。容器仍需调用相等比较器。
18.4 暴露不稳定顺序
序列化、测试和日志如果要求确定顺序,应先排序键或使用 map。
19. 面试总结
unordered_map 是哈希键值容器,平均查找、插入和删除复杂度为 O(1),但不保证遍历顺序,最坏情况可能退化为 O(n)。其性能受哈希函数、桶数量和装载因子影响。
使用时应注意 operator[] 会在键不存在时插入默认值,提前知道规模时可调用 reserve(),复杂值构造建议使用 try_emplace()。