目录
- 前言
- 一、先搞清楚——有序 vs 无序
- 二、unordered_map 核心 API 速查
- 三、unordered_set 核心 API 速查
- 四、自定义哈希函数——我的 key 我做主
- 五、实战案例
- 六、常见踩坑与避坑指南
- 七、性能对比实测
- 八、总结
前言
写一个单词频率统计程序,你用
map<string, int>,运行后发现比同事的 Python dict 慢了一截。一查资料才发现——map底层是红黑树,查找 O(log n);而 Python 的 dict 是哈希表,查找 O(1)。C++ 有没有哈希表版本的 map?当然有,它就是本文的主角——
unordered_map。
C++11 引入了四大无序关联容器:unordered_map、unordered_set、unordered_multimap、unordered_multiset。本文聚焦最常用的前两个,从使用技巧讲到避坑指南,带你彻底玩转它们。
一、先搞清楚——有序 vs 无序
在学 unordered_map 之前,先搞明白它和 map 的本质区别:
| map / set | unordered_map / unordered_set | |
|---|---|---|
| 底层结构 | 红黑树(平衡二叉搜索树) | 哈希表(链地址法) |
| 元素顺序 | 按 key 自动排序 | 无序(按哈希值分布) |
| 查找复杂度 | O(log n) | 平均 O(1),最坏 O(n) |
| 插入复杂度 | O(log n) | 平均 O(1),最坏 O(n) |
| 内存占用 | 较小 | 较大(需要维护桶数组 + 链表) |
| 适用场景 | 需要有序遍历 | 需要快速增删查,不关心顺序 |
一句话总结:要排序用 map,要速度用 unordered_map。
二、unordered_map 核心 API 速查
2.1 头文件与构造
#include <unordered_map>
#include <string>
using namespace std;
// 默认构造
unordered_map<string, int> umap1;
// 初始列表构造
unordered_map<string, int> umap2 = {
{"apple", 3},
{"banana", 5},
{"cherry", 2}
};
// 拷贝构造
unordered_map<string, int> umap3(umap2);
// 指定初始桶大小
unordered_map<string, int> umap4(64); // 预分配64个桶,减少rehash
2.2 插入元素
unordered_map<string, int> umap;
// 方式1:insert + pair
umap.insert(pair<string, int>("apple", 3));
// 方式2:insert + make_pair
umap.insert(make_pair("banana", 5));
// 方式3:insert + 初始化列表(最简洁)
umap.insert({"cherry", 2});
// 方式4:operator[] —— 最常用!
umap["durian"] = 8; // 不存在则插入,存在则修改
umap["apple"] = 10; // apple已存在,将值从3改为10
// 方式5:emplace —— 原地构造,效率最高
umap.emplace("elderberry", 4);
insert vs operator[] vs emplace:
| 方法 | key已存在时 | key不存在时 | 返回值 |
|---|---|---|---|
insert | 插入失败 | 插入成功 | pair<iterator, bool> |
operator[] | 覆盖旧值 | 插入(value默认构造,再赋值) | mapped_type& |
emplace | 插入失败 | 原地构造插入 | pair<iterator, bool> |
注意:
operator[]在 key 不存在时会先用默认构造函数创建一个 value,再赋值。如果 value 类型没有默认构造函数,编译报错。此时必须用insert或emplace。
2.3 查找元素
unordered_map<string, int> umap = {{"apple", 3}, {"banana", 5}};
// 方式1:find —— 最常用
auto it = umap.find("apple");
if (it != umap.end())
{
cout << "找到了: " << it->second << endl;
}
// 方式2:count —— 判断key是否存在(返回0或1)
if (umap.count("banana"))
{
cout << "banana 存在" << endl;
}
// 方式3:contains (C++20) —— 语义更清晰
if (umap.contains("cherry")) // C++20
{
cout << "cherry 存在" << endl;
}
// 方式4:operator[] + 判断(有副作用!)
// 不推荐用于查找,因为key不存在时会偷偷插入!
// int val = umap["durian"]; // durian 被悄悄插入了!
find vs count 怎么选:只需判断存在用
count(或 C++20 的contains);需要访问 value 用find。
2.4 删除元素
unordered_map<string, int> umap = {{"a", 1}, {"b", 2}, {"c", 3}};
// 按 key 删除 —— 返回删除的元素个数(0 或 1)
size_t n = umap.erase("a"); // n = 1
size_t m = umap.erase("nonexist"); // m = 0
// 按迭代器删除 —— 返回被删元素的下一个迭代器
auto it = umap.find("b");
if (it != umap.end())
{
auto next = umap.erase(it); // next 指向 "c"
}
// 清空
umap.clear();
2.5 遍历
unordered_map<string, int> umap = {{"a", 1}, {"b", 2}, {"c", 3}};
// 方式1:迭代器遍历
for (auto it = umap.begin(); it != umap.end(); ++it)
{
cout << it->first << " : " << it->second << endl;
}
// 方式2:范围for(C++11,推荐)
for (const auto& kv : umap)
{
cout << kv.first << " : " << kv.second << endl;
}
// 方式3:结构化绑定(C++17)
for (const auto& [key, value] : umap)
{
cout << key << " : " << value << endl;
}
重要:遍历顺序是无序的,不一定等于插入顺序,每次运行可能不同。
2.6 容量与桶信息
unordered_map<string, int> umap;
umap.empty(); // 是否为空
umap.size(); // 元素个数
umap.max_size(); // 理论上能容纳的最大元素数
// 桶相关 —— 调试和性能调优时用
umap.bucket_count(); // 桶的数量
umap.max_bucket_count(); // 最大桶数
umap.bucket("apple"); // 查询 "apple" 在哪个桶
umap.bucket_size(0); // 第0号桶中有几个元素
// 负载因子
umap.load_factor(); // 当前负载因子 = size / bucket_count
umap.max_load_factor(); // 最大负载因子(默认1.0)
umap.max_load_factor(0.7); // 设置最大负载因子为0.7
三、unordered_set 核心 API 速查
unordered_set 的 API 与 unordered_map 高度一致,区别只在于只存 key,没有 value。
3.1 基本操作
#include <unordered_set>
unordered_set<int> uset = {1, 2, 3, 4, 5};
// 插入
uset.insert(6);
uset.emplace(7);
// 查找
auto it = uset.find(3);
if (uset.count(4)) { /* 存在 */ }
// 删除
uset.erase(1); // 按值删除
uset.erase(uset.begin()); // 按迭代器删除
// 遍历
for (const auto& val : uset)
{
cout << val << " ";
}
3.2 去重神器
unordered_set 天然去重,常用于批量去重场景:
vector<int> nums = {1, 2, 2, 3, 3, 3, 4, 5, 5};
unordered_set<int> uset(nums.begin(), nums.end());
// uset 中只剩 {1, 2, 3, 4, 5},顺序不确定
vector<int> unique_nums(uset.begin(), uset.end());
四、自定义哈希函数——我的 key 我做主
4.1 问题场景
// 这行代码能编译吗?
unordered_map<pair<int, int>, string> umap; // ❌ 编译错误!
为什么报错?因为 C++ 标准库没有为 pair<int, int> 提供默认的哈希函数。你需要自己写一个。
4.2 三种自定义方式
方式一:仿函数(推荐)
struct PairHash
{
size_t operator()(const pair<int, int>& p) const
{
size_t h1 = hash<int>()(p.first);
size_t h2 = hash<int>()(p.second);
// 经典组合哈希:h = h1 * 质数 + h2
return h1 * 131 + h2;
}
};
unordered_map<pair<int, int>, string, PairHash> umap;
umap[{1, 2}] = "hello"; // 可以正常编译运行了
方式二:特化 std::hash(不推荐侵入标准命名空间)
namespace std
{
template<>
struct hash<pair<int, int>>
{
size_t operator()(const pair<int, int>& p) const
{
return hash<int>()(p.first) * 131 + hash<int>()(p.second);
}
};
}
方式三:Lambda(C++20)
auto hash_func = [](const pair<int, int>& p) -> size_t {
return p.first * 131 + p.second;
};
// 注意:unordered_map 的 Hash 模板参数需要可默认构造的类型,lambda不行
// 除非使用 decltype,但比较繁琐,推荐方式一
4.3 自定义等值比较
如果你的 key 需要自定义比较规则(不只是用 ==),可以传入第四个模板参数:
struct CaseInsensitiveEqual
{
bool operator()(const string& a, const string& b) const
{
// 忽略大小写比较
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); ++i)
{
if (tolower(a[i]) != tolower(b[i])) return false;
}
return true;
}
};
struct CaseInsensitiveHash
{
size_t operator()(const string& s) const
{
string lower;
for (char c : s) lower += tolower(c);
return hash<string>()(lower);
}
};
// 四个模板参数:Key, Value, Hash, KeyEqual
unordered_map<string, int, CaseInsensitiveHash, CaseInsensitiveEqual> umap;
五、实战案例
5.1 单词频率统计(经典面试题)
#include <iostream>
#include <unordered_map>
#include <vector>
#include <sstream>
using namespace std;
unordered_map<string, int> wordCount(const string& text)
{
unordered_map<string, int> freq;
istringstream iss(text);
string word;
while (iss >> word)
{
++freq[word]; // operator[]:不存在则自动插入并初始化为0
}
return freq;
}
int main()
{
string text = "apple banana apple cherry banana apple";
auto result = wordCount(text);
for (const auto& [word, count] : result)
{
cout << word << ": " << count << endl;
}
return 0;
}
5.2 两数之和(LeetCode 1)
vector<int> twoSum(vector<int>& nums, int target)
{
unordered_map<int, int> indexMap; // 值 -> 下标
for (int i = 0; i < nums.size(); ++i)
{
int complement = target - nums[i];
auto it = indexMap.find(complement);
if (it != indexMap.end())
{
return {it->second, i};
}
indexMap[nums[i]] = i;
}
return {};
}
时间复杂度 O(n),如果用 map 是 O(n log n)。这 10 倍的差距就是 unordered_map 存在的意义。
5.3 用 unordered_set 实现交集 / 并集
vector<int> intersection(const vector<int>& a, const vector<int>& b)
{
unordered_set<int> set_a(a.begin(), a.end());
vector<int> result;
for (int num : b)
{
if (set_a.count(num)) // b中的元素在a中存在
{
result.push_back(num);
set_a.erase(num); // 去重:每个交集元素只出现一次
}
}
return result;
}
vector<int> union_set(const vector<int>& a, const vector<int>& b)
{
unordered_set<int> result(a.begin(), a.end());
result.insert(b.begin(), b.end());
return vector<int>(result.begin(), result.end());
}
5.4 预分配桶大小——避免重复 rehash
当你大概知道要插入多少元素时,提前分配桶可以显著提升性能:
unordered_map<int, string> umap;
umap.reserve(100000); // 预分配足够的桶,避免多次 rehash
for (int i = 0; i < 100000; ++i)
{
umap.emplace(i, "value"); // 全程不会触发 rehash
}
reserve(n) 会调整 bucket_count 使得至少可以容纳 n 个元素而不触发 rehash。而 rehash(n) 直接将桶数设为至少为 n。
六、常见踩坑与避坑指南
坑1:遍历时修改容器
// ❌ 错误:遍历中删除会导致迭代器失效
for (const auto& kv : umap)
{
if (kv.second == 0)
{
umap.erase(kv.first); // 迭代器失效,程序崩溃
}
}
// ✅ 正确:C++20 erase_if 或手动处理
for (auto it = umap.begin(); it != umap.end(); )
{
if (it->second == 0)
{
it = umap.erase(it); // erase 返回下一个有效迭代器
}
else
{
++it;
}
}
坑2:用 operator[] 查找导致意外插入
void printIfExists(unordered_map<string, int>& umap, const string& key)
{
// ❌ 错误:umap[key] 在 key 不存在时会偷偷插入!
// cout << umap[key] << endl;
// ✅ 正确:用 find
auto it = umap.find(key);
if (it != umap.end())
{
cout << it->second << endl;
}
}
坑3:pair 做 key 忘记写哈希函数
// ❌ 编译错误
unordered_map<pair<int, int>, string> umap;
// ✅ 自定义哈希
struct PairHash { /* ... */ };
unordered_map<pair<int, int>, string, PairHash> umap;
坑4:浮点数做 key
unordered_map<double, string> umap; // 危险!
浮点数的 == 比较和哈希值计算都受精度影响,0.1 + 0.2 和 0.3 可能哈希到不同的桶。尽量不要用浮点数做 key。
七、性能对比实测
用一个简单的 benchmark 感受一下差距:
#include <chrono>
// 100万个随机插入 + 查找
| 容器 | 插入 100万 元素 | 查找 100万 次 | 有序遍历 |
|---|---|---|---|
map | ~780ms | ~650ms | O(n),天然有序 |
unordered_map | ~420ms | ~280ms | 不支持 |
数据仅供参考,实际性能与 key 类型的哈希开销、负载因子设置、是否触发 rehash 等因素相关。
如果你的场景需要频繁地增删查且不关心顺序,选 unordered_map 就对了。如果需要按 key 排序输出,map 是不二之选。
八、总结
本文覆盖了 unordered_map 和 unordered_set 从入门到进阶的核心知识点:
| 知识点 | 要点 |
|---|---|
| 底层结构 | 哈希表,平均 O(1) 增删查 |
| 常用 API | insert / emplace / find / count / erase / operator[] |
| 遍历 | 范围 for / 迭代器,遍历顺序无序 |
| 自定义哈希 | 传入函数对象作为第三个模板参数 |
| 自定义比较 | 传入函数对象作为第四个模板参数 |
| 性能调优 | reserve 预分配、max_load_factor 控制负载因子 |
| 常见坑 | operator[] 的副作用、遍历删除、对组做 key 缺哈希函数 |
理解原理 ,掌握 API,知道常见的坑在哪里,就算是真正玩转了 C++ 的哈希容器。
本文是个人学习笔记,如有疏漏,欢迎评论区指正讨论。
704



被折叠的 条评论
为什么被折叠?



