std::unordered_map 是 C++ 标准模板库(STL)中一个强大的无序关联容器。其底层基于哈希表实现,核心优势在于提供了平均 O(1) 时间复杂度的查找、插入和删除操作(最坏情况下为 O(n))。与基于红黑树实现、元素有序存储的 std::map 不同,std::unordered_map 中的键值对存储顺序由键的哈希值决定。
核心特性
- 键唯一性:键(key)不可重复,尝试插入重复键会覆盖原有值或导致插入失败。
- 无序性:元素顺序不固定,不依赖于键的大小。
- 哈希依赖:键的类型必须支持哈希计算(内置类型默认支持,自定义类型需提供哈希函数)。
- 非线程安全:多线程环境下进行操作需要手动加锁或使用并发容器。
头文件与命名空间
使用前需要包含对应的头文件,它默认位于 std 命名空间中。
#include <unordered_map> // 核心头文件
using namespace std; // 可选,若不使用则需写为 std::unordered_map
基本语法与操作
1. 定义与初始化
// 1. 创建空容器
unordered_map<int, string> umap1;
// 2. 使用初始化列表
unordered_map<int, string> umap2 = {
{1, "apple"},
{2, "banana"},
{3, "cherry"}
};
// 3. 拷贝构造
unordered_map<int, string> umap3(umap2);
// 4. 移动构造(C++11及以上)
unordered_map<int, string> umap4(std::move(umap2)); // umap2 的内容被移走,变为空容器
2. 插入元素
推荐优先使用 emplace,它可以直接在容器内构造元素,减少不必要的拷贝开销。
unordered_map<int, string> umap;
// 方式1:使用 [] 运算符(若键不存在则插入,存在则覆盖其值)
umap[1] = "apple"; // 插入 {1, "apple"}
umap[1] = "apricot"; // 覆盖为 {1, "apricot"}
// 方式2:使用 insert(若键已存在则插入失败,不会覆盖)
auto ret = umap.insert({2, "banana"}); // 插入成功,ret.first指向元素,ret.second = true
ret = umap.insert({2, "blueberry"}); // 插入失败,ret.second = false
// 方式3:使用 emplace(原地构造,效率更高)
umap.emplace(3, "cherry"); // 直接构造 {3, "cherry"}
3. 查找元素
find(key):返回指向键为 key 的元素的迭代器,若不存在则返回 end()。
count(key):返回键的存在次数(由于键唯一,结果为0或1)。
- 不推荐使用
[] 运算符进行纯查找,因为若键不存在,它会自动插入一个带有默认值的元素。
unordered_map<int, string> umap = {{1, "apple"}, {2, "banana"}};
// 方式1:使用 find(推荐)
auto it = umap.find(1);
if (it != umap.end()) {
cout << "找到:" << it->first << " -> " << it->second << endl; // 输出:1 -> apple
} else {
cout << "未找到" << endl;
}
// 方式2:使用 count
if (umap.count(2)) {
cout << "键 2 存在" << endl;
}
4. 删除元素
erase(key):删除键为 key 的元素,返回删除的个数(0或1)。
erase(iterator):删除迭代器指向的元素。
clear():清空容器中所有元素。
5. 遍历元素
支持迭代器遍历和范围 for 循环(C++11起)。
unordered_map<int, string> umap = {{1, "apple"}, {2, "banana"}, {3, "cherry"}};
// 方式1:范围 for 循环(推荐,简洁)
for (const auto& pair : umap) {
cout << pair.first << " -> " << pair.second << endl;
}
// 方式2:迭代器遍历
for (auto it = umap.begin(); it != umap.end(); ++it) {
cout << it->first << " -> " << it->second << endl;
}
6. 常用成员函数
| 函数 |
功能说明 |
size() |
返回元素个数 |
empty() |
判断容器是否为空 |
max_size() |
返回容器可容纳的最大元素数量 |
clear() |
清空所有元素 |
swap() |
交换两个 unordered_map 的内容 |
bucket_count() |
返回哈希桶的数量 |
load_factor() |
返回当前负载因子(元素数 / 桶数) |
rehash(n) |
将哈希桶数量设置为至少 n 个 |
reserve(n) |
预留空间,使容器至少能容纳 n 个元素而不导致扩容 |
unordered_map<int, string> umap = {{1, "apple"}, {2, "banana"}};
cout << "元素个数:" << umap.size() << endl; // 输出 2
cout << "是否为空:" << boolalpha << umap.empty() << endl; // 输出 false
// 提前预留空间,可以有效减少哈希表扩容带来的性能开销,这是性能优化的常用手段。
umap.reserve(100);
自定义类型作为键
若要将自定义的结构体或类作为键,必须满足两个条件:
- 重载
== 运算符(用于判断键是否相等)。
- 提供哈希函数(可以特化
std::hash,或在模板参数中传入自定义哈希函数对象)。
示例:将 Person 结构体作为键
#include <functional> // 用于 std::hash
// 自定义类型
struct Person {
string name;
int age;
// 必须重载 == 运算符
bool operator==(const Person& other) const {
return name == other.name && age == other.age;
}
};
// 特化 std::hash<Person>(C++11及以上)
namespace std {
template<>
struct hash<Person> {
size_t operator()(const Person& p) const {
// 组合成员变量的哈希值
size_t h1 = hash<string>()(p.name);
size_t h2 = hash<int>()(p.age);
return h1 ^ (h2 << 1); // 简单的组合方式,实际项目中需注意避免哈希碰撞
}
};
}
// 使用自定义键类型
int main() {
unordered_map<Person, string> umap;
umap[{"Alice", 20}] = "student";
umap[{"Bob", 25}] = "engineer";
// 查找
auto it = umap.find({"Alice", 20});
if (it != umap.end()) {
cout << it->first.name << ": " << it->second << endl; // 输出 Alice: student
}
return 0;
}
重要注意事项与性能考量
- 哈希碰撞:如果哈希函数设计不当,可能导致大量元素被映射到同一个哈希桶中,使得性能退化为 O(n)。深入理解数据结构中哈希表的原理对设计好的哈希函数至关重要。
- 迭代器稳定性:插入或删除元素可能引发哈希表扩容(rehash),这会导致除当前被删除迭代器外的所有迭代器失效。
- 与
std::map 的核心对比:
std::map:基于红黑树,元素有序,查找/插入/删除时间复杂度为 O(log n),键需支持 < 运算符比较。
std::unordered_map:基于哈希表,元素无序,平均时间复杂度为 O(1),键需支持哈希计算和 == 比较。
- 性能优化建议:
- 使用
reserve(n) 提前预留足够空间,避免多次扩容。
- 为自定义类型设计分布均匀、低碰撞率的哈希函数。
- 避免在性能关键路径上进行频繁的插入和删除,这可能引发不必要的哈希表重构。在多线程环境下操作时,需要注意并发控制策略。
总结:如何选择 std::map 与 std::unordered_map?
std::unordered_map 和 std::map 是 C++ 中最常用的两种关联容器,选择取决于具体场景和需求。
优先考虑使用 std::map 的场景:
- 需要按键进行有序遍历或范围查找(如查找键在某个区间内的所有元素)。
- 对内存占用比较敏感,或者键的分布容易导致严重的哈希碰撞。
- 需要保证最坏情况下的性能稳定性(O(log n) 比 O(n) 更可控)。
优先考虑使用 std::unordered_map 的场景:
- 主要进行精确查找、插入、删除操作,并且追求极致的平均性能(O(1))。
- 元素的存储顺序无关紧要,且键的哈希特性良好。
- 数据量较大,并能通过
reserve() 提前分配好空间以最小化扩容开销。
| 特性 |
std::map |
std::unordered_map |
| 底层实现 |
红黑树 |
哈希表 |
| 元素顺序 |
有序(按键排序) |
无序 |
| 时间复杂度 |
O(log n) |
平均 O(1),最坏 O(n) |
| 键的要求 |
需支持 < 比较 |
需支持 == 比较和哈希计算 |
| 内存占用 |
通常较低 |
通常较高(维护哈希桶) |
| 典型用例 |
排行榜、区间查询、需要有序性的场景 |
缓存、字典、高频精确查找场景 |
最终建议:若需要有序性或范围查找,选择 std::map;若仅需高效的精确查找且可以接受无序,则 std::unordered_map 通常是性能更好的选择。在复杂系统中,合理利用容器特性是进行性能优化的重要环节。