前言
std::map 和 std::unordered_map 是 C++ STL 中最常用的两种关联容器,它们都用于存储 key-value 键值对,且 key 唯一。两者功能非常相似,但底层实现、性能特性、适用场景完全不同。接下来我将详细讲解两者的区别,以及分别在什么场景下使用:
一、两者的区别
先看结论,了解 std::map 与std::unordered_map 的核心区别:
| 对比维度 | std::map | std::unordered_map |
|---|---|---|
| 底层数据结构 | 红黑树 | 哈希表 |
| 元素有序性 | 有序(按key升序排列) | 无序(遍历顺序不固定) |
| 查找/插入/删除时间复杂度 | O(log n) | 平均 O(1),最坏 O(n) |
| 内存占用 | 较高(每个节点需 3 个指针 + 颜色) | 一般较高(桶 + 负载因子) |
| key 要求 | 需要 < 比较(或自定义comparator) | 需要 == 比较 + std::hash |
| 适用数据量 | 中小数据量(几千~几万) | 大数据量(几十万以上) |
| 典型使用场景 | 需要有序、范围查询、稳定性能 | 追求极致速度、不关心顺序 |
二、底层实现原理
1.std::map ------ 红黑树
属于有序关联容器,内部维护一棵自平衡的二叉查找树。插入、删除、查找时自动调整树形,保证树的高度始终为 O(log n),因此所有操作都有严格的 O(log n) 上界,性能非常稳定。
2.std::unordered_map ------ 哈希表
属于无序关联容器,内部是一个桶数组 + 链表/链地址法(C++11 后)。通过 std::hash 计算 key 的哈希值,决定存放在哪个桶。平均情况下 每个桶的元素数量很少(查找几乎是 O(1)),但当哈希冲突严重或负载因子过高时,会发生 rehash(重新分配桶),最坏情况退化为 O(n)。
三、差异详细讲解
1. 有序性与遍历顺序
std::map:迭代器遍历时严格按 key 排序(默认 std::less)。
std::unordered_map:迭代器遍历顺序与插入顺序无关,取决于哈希值和桶的分布。即使同一个程序多次运行,遍历顺序也可能不同。
2. 时间复杂度与性能
查找(find、count、operator[]):
map:O(log n);
unordered_map:平均 O(1),最坏 O(n)。
插入/删除:同上。
范围查询(如查找 [low, high] 区间所有元素):
map:lower_bound + upper_bound 非常高效;
unordered_map:无法高效实现,只能线性扫描。
3. 内存与缓存友好性
map:节点分散在堆上,缓存不友好(指针跳跃)。
unordered_map:桶数组连续,缓存相对友好,但负载因子(默认 1.0)会导致额外内存浪费。
4. key 类型要求
std::map:
cpp
// 只需实现 operator< 即可(或提供 Comparator)
bool operator<(const MyKey& other) const;
std::unordered_map:
cpp
// 必须同时提供 hash 和 ==(或 equal_to)
struct MyHash { size_t operator()(const MyKey& k) const; };
struct MyEqual { bool operator()(const MyKey& a, const MyKey& b) const; };
内置类型(int、string等)已提供。
5. 迭代器失效
map:仅被删除的迭代器失效,其他迭代器仍有效。
unordered_map:rehash 时所有迭代器都可能失效(insert、rehash、clear 等都可能触发)。
6. 独有成员函数
std::map 特有:
lower_bound、upper_bound、equal_range、key_comp()、value_comp()
std::unordered_map 特有:
bucket_count()、load_factor()、max_load_factor()、rehash()、reserve()
bash
------------std::map 独有成员函数---------------
lower_bound(key) //返回第一个不小于 key 的元素迭代器,用于有序范围查找。
upper_bound(key) //返回第一个大于 key 的元素迭代器,配合 lower_bound 可获取等于 key 的元素范围。
equal_range(key) //返回一对迭代器,分别指向等于 key 的元素范围的首尾(等价于 {lower_bound, upper_bound} )。
key_comp() //返回用于比较键的比较器对象,用于自定义键的排序逻辑。
value_comp() //返回用于比较键值对的比较器对象,本质是基于 key_comp() 封装。
-------------std::unordered_map 独有成员函数-------------
bucket_count() //返回当前哈希桶的总数。
load_factor() //返回当前负载因子(元素总数 / 桶数),用于衡量哈希表的拥挤程度。
max_load_factor() //获取或设置允许的最大负载因子,超过则自动触发 rehash 。
rehash(n) //重新哈希,将桶数调整为至少 n ,会重新分配所有元素。
reserve(n) //预分配空间,使容器能容纳至少 n 个元素而不触发 rehash ,优化插入性能。
四、std::map 与std::unordered_map的使用场景
推荐使用 std::map 的场景
-
需要维护有序性
日志按时间戳排序输出;
字典/配置按 key 字母顺序展示;
需要按 key 范围查询(如查找所有 ID 在 [1000, 2000] 的记录)。
-
对最坏情况性能敏感
实时系统、游戏引擎、服务器核心路径;
不能容忍哈希碰撞导致的 O(n) 卡顿。
-
数据量不大(< 10万量级)
log n 在现代 CPU 上极快(log₂(100000) ≈ 17)。
-
key 难以设计好的 hash 函数
自定义复杂结构体、对象指针等。
-
需要频繁做范围操作
区间统计、滑动窗口类问题。
推荐使用 std::unordered_map 的场景
-
追求极致速度
大规模数据去重、计数(LeetCode 频率统计题);
缓存(LRU Cache 通常配合 list + unordered_map)。
-
数据量巨大(10万 ~ 千万级)
此时 O(1) 的优势非常明显。
-
不关心顺序
仅需快速 insert / find / erase。
-
key 是内置类型或容易哈希
int、string、uint64_t、pair<int,int> 等。
-
内存不是瓶颈
愿意用空间换时间。
代码示例:
cpp
#include <iostream>
#include <map>
#include <unordered_map>
#include <string>
using namespace std;
int main() {
// 1. map 示例
map<string, int> m;
m["apple"] = 5;
m["banana"] = 3;
m["cherry"] = 8;
for (const auto& p : m) {
cout << p.first << ": " << p.second << endl; // 按字母顺序输出
}
// 范围查询
auto it = m.lower_bound("b");
auto end = m.upper_bound("c");
// it 到 end 就是 [b, c] 区间
// 2. unordered_map 示例
unordered_map<string, int> um;
um["apple"] = 5;
um["banana"] = 3;
um["cherry"] = 8;
// 查找极快
if (um.find("banana") != um.end()) {
cout << "found!\n";
}
// 自定义类型示例(unordered_map)
struct Person { string name; int age; };
struct Hash {
size_t operator()(const Person& p) const {
return hash<string>{}(p.name) ^ p.age;
}
};
unordered_map<Person, double, Hash> salary;
}
五、总结
简单来说:
std::map = 稳定、有序、可靠(红黑树)
std::unordered_map = 极快、无序、需要精心设计 hash(哈希表)
在实际项目中,大部分的场景下 unordered_map 更快,但关键路径、需要有序或最坏情况稳定的地方,必须用 std::map。