std::unordered_map
1. 概述
-
定义:std::unordered_map<Key, T, Hash, KeyEqual, Allocator> 是一个基于哈希表(hash table)实现的键值对容器,提供平均 O(1) 的查找、插入和删除性能。
-
特点:
-
无序:元素按哈希值分布在若干“桶”(bucket)中,不保证遍历顺序。
-
唯一键:同一个键最多只能出现一次;如果插入已存在的键,默认不覆盖(可用 operator[] 或 at 修改)。
-
可自定义:支持自定义哈希函数和键相等比较器,也可指定内存分配器。
-
2. 内部实现
-
哈希桶结构
-
容器内部维护一组“桶”,每个桶是一个链表(或其他冲突解决结构)。
-
元素根据 Hash(key) % bucket_count 落入对应桶中。
-
-
装载因子(load factor)
-
定义为 size() / bucket_count,表示平均每个桶的元素数。
-
当装载因子超过 max_load_factor() 时,容器会自动进行 rehash(扩容并重新分配桶)。
-
-
再哈希(rehash)
-
调用 rehash(n) 会将桶数调整为 ≥ n,并将所有元素重新分布。
-
reserve(n) 则是以元素数 n 为基准,确保 bucket_count ≥ n / max_load_factor()。
-
3. 性能特征
操作 | 平均复杂度 | 最坏情况复杂度 | 备注 |
---|---|---|---|
find / operator[] | O(1) | O(n) | 取决于哈希冲突;若所有元素落在同一桶,退化为链表查找 |
insert / emplace | O(1) | O(n) | 同上,且可能触发一次 rehash(O(n)) |
erase(key) | O(1) | O(n) | 同上 |
clear | O(n) | O(n) | |
遍历(iteration) | O(n + bucket_count) | O(n + bucket_count) | 需跳过空桶 |
-
空间开销:
- 每个桶需存储一个指针或链表头;元素节点通常包含键、值、下一个节点指针,以及可能的桶索引/哈希缓存。
-
哈希函数好坏:
- 高质量的哈希函数能显著降低冲突,提高稳定性;如对自定义类型可使用 std::hash 或第三方的高性能哈希。
4. 常用 API
// 构造与析构
std::unordered_map<Key, T> m1; // 默认构造
std::unordered_map<Key, T> m2(100); // 指定初始桶数
std::unordered_map<Key, T> m3(100, MyHash{}, MyEqual{}); // 指定哈希与比较
// 大小与容量
bool empty() const;
size_t size() const;
size_t bucket_count() const;
float load_factor() const;
float max_load_factor() const;
void rehash(size_t newBucketCount);
void reserve(size_t count); // 保证能插入 count 个元素而不触发 rehash
// 元素访问
T& operator[](const Key& key); // 若不存在则插入默认构造的 T
T& at(const Key& key); // 若不存在抛出 std::out_of_range
// 插入
std::pair<iterator, bool> insert(const value_type& kv);
template< class... Args >
std::pair<iterator, bool> emplace(Args&&... args);
// 查找与删除
iterator find(const Key& key);
size_t erase(const Key& key);
iterator erase(iterator pos);
void clear();
// 遍历
iterator begin() noexcept;
iterator end() noexcept;
const_iterator cbegin() const noexcept;
const_iterator cend() const noexcept;
// 哈希与比较器查询
size_t bucket(const Key& key) const;
size_t bucket_size(size_t n) const;
Hash hash_function() const;
KeyEqual key_eq() const;
5. 使用示例
#include <unordered_map>
#include <string>
#include <iostream>
int main() {
// 1. 创建并插入
std::unordered_map<std::string, int> wordCount;
wordCount["hello"] = 1; // operator[] 插入或修改
wordCount.insert({"world", 2}); // insert 不会覆盖已存在键
wordCount.emplace("foo", 3); // 完美转发构造
// 2. 查找
auto it = wordCount.find("world");
if (it != wordCount.end()) {
std::cout << it->first << ": " << it->second << "\n";
}
// 3. 遍历
for (auto& kv : wordCount) {
std::cout << kv.first << " => " << kv.second << "\n";
}
// 4. 保证容量
wordCount.reserve(1000); // 预计要插入 1000 条记录,减少 rehash 次数
// 5. 统计装载因子
std::cout << "load factor: " << wordCount.load_factor() << "\n";
return 0;
}
6. 自定义哈希与相等比较
当键类型为自定义结构体时,需要提供 Hash 和 KeyEqual:
struct Point { int x, y; };
// 自定义哈希:结合 x,y 的值
struct PointHash {
size_t operator()(Point const& p) const noexcept {
// 经典做法:位移与异或
return std::hash<int>()(p.x) ^ (std::hash<int>()(p.y) << 1);
}
};
// 自定义相等比较
struct PointEqual {
bool operator()(Point const& a, Point const& b) const noexcept {
return a.x == b.x && a.y == b.y;
}
};
std::unordered_map<Point, std::string, PointHash, PointEqual> mp;
7. 注意事项与优化
-
避免频繁 rehash
- 使用 reserve() 预先分配足够桶数,比频繁自动扩容更高效。
-
迭代顺序不稳定
- 容器内部槽位和元素分布会随 rehash 而变化,不要依赖遍历顺序。
-
哈希攻击
- 在对抗恶意输入的场景中,默认 std::hash 可能遭受冲突攻击,可考虑使用带随机种子的哈希或第三方库(如 CityHash、MurmurHash)。
-
线程安全
- 读写同一容器需加锁;C++20 起可在不同线程同时读不同桶,但标准未强制保证,生产环境仍建议加互斥。
-
内存占用
- 哈希表相较于红黑树(std::map)通常占用更多内存;应在性能 vs. 空间之间权衡。
8. 使用建议
-
需要
-
快速查找/插入/删除 且无需有序遍历时,首选 std::unordered_map。
-
容器大小可预估,且希望通过 reserve() 控制 rehash 时机。
-
-
不需要
-
保持元素有序或按键排序输出时,应选用 std::map。
-
需要对容器做范围算法(如二分查找)或有序区间操作时。
-
9. emplace和insert异同
相同点
-
功能
-
都是在 std::unordered_map 中插入元素(键值对)。
-
若指定键已存在,插入操作不生效,都会返回相同的迭代器和 false。
-
-
返回值
- std::pair<iterator,bool>——iterator 指向新插入或已有元素的位置,bool 表示此次调用是否实际插入了新元素。
不同点
特性 | insert | emplace |
---|---|---|
接口签名 | 多重重载,典型如 insert(const value_type&)、insert(value_type&&)、insert(initializer_list<value_type>) | template<class… Args> emplace(Args&&… args) |
参数 | 需要先构造好 value_type(即 pair<const Key, T>)再传入,可能多一次拷贝/移动 | 直接将参数完美转发(perfect‑forward)给底层元素构造函数,就地构造,避免不必要的临时对象 |
构造方式 | 拷贝 或 移动 已有的 pair | 原地(in‑place)调用 pair 的构造函数 |
效率 | 如果要构造 pair,就至少一次临时对象(拷贝/移动) | 零或更少的拷贝/移动,适合复杂类型或昂贵拷贝的场景 |
复杂构造支持 | 只能插入已经准备好的 pair | 支持 piecewise_construct,可以分别传递给 key 和 value 的构造参数 |
例子对比
std::unordered_map<std::string, std::vector<int>> m;
// 1. insert:需要先构造一个 pair,再插入
std::pair<const std::string, std::vector<int>> p("key", {1,2,3});
auto res1 = m.insert(p); // 拷贝 p
auto res2 = m.insert({ "key2", {4,5,6} }); // 构造临时 pair,再移动或拷贝
// 2. emplace:直接传递构造参数,就地构造
auto res3 = m.emplace("key3", std::vector<int>{7,8,9});
// 等价于:在内部执行 pair("key3", vector{7,8,9}) 的就地构造,无额外拷贝
// 3. emplace + piecewise_construct(针对 key 和 value 各自参数包更复杂的场景)
struct Key { Key(int,a){} };
struct Val { Val(double,b){} };
std::unordered_map<Key, Val> m2;
m2.emplace(
std::piecewise_construct,
std::forward_as_tuple(123), // Key(int)
std::forward_as_tuple(3.14) // Val(double)
);
何时优先使用哪种?
-
简单场景:插入已有 pair 或者初学者,为了可读性,用 insert 也很直观。
-
性能敏感或避免临时:当 T 构造/拷贝/移动开销较大时,优先 emplace。
-
复杂构造:需要传多个参数给 key 或 value 的构造函数时,emplace(尤其配合 piecewise_construct)更加灵活。