map 是 C++ STL 中最核心的关联式容器之一,以「键值对(key-value)」形式存储数据,核心特性是「键唯一、自动按键升序排列」。底层基于红黑树(RB-Tree) 实现,保证插入、删除、查找操作的时间复杂度均为 O(logn),是处理「有序键值对映射」的首选容器。
一、核心特性(必知)
| 特性 | 详细说明 |
|---|---|
| 存储形式 | 键值对(pair<const Key, T>),键(key)唯一,值(value)可重复 |
| 有序性 | 自动按「键」的升序排列(可自定义排序规则,如降序、结构体键的自定义排序) |
| 底层实现 | 红黑树(平衡二叉搜索树),与 set 共享底层红黑树实现,仅存储类型不同 |
| 键的特性 | 键是 const 类型,不可修改(修改键会破坏红黑树有序性),只能通过删除 + 重新插入修改 |
| 迭代器类型 | 双向迭代器(支持 ++/--,不支持随机访问如 it+3) |
| 内存与迭代器 | 插入 / 删除元素时,除被删除元素的迭代器外,其他迭代器不失效 |
| 访问方式 | 支持 [] 运算符、at() 方法(按键访问),无下标索引(如 map[0] 是按键 0 访问,非位置) |
二、底层实现原理
map 的底层是红黑树,与 set 的核心区别在于:
set存储的是单一值T,红黑树的节点值就是T;map存储的是pair<const Key, T>,红黑树以「键(Key)」为排序依据,保证键的唯一性和有序性。
红黑树与 map 的映射
- 插入键值对时,红黑树按
Key的大小排序,相同Key会被拒绝(insert返回false); - 查找操作通过
Key遍历红黑树,时间复杂度O(logn); - 迭代器遍历
map本质是红黑树的中序遍历,结果为键的升序序列。
三、基础使用(核心操作)
1. 头文件与命名空间
使用 map 必须包含 <map> 头文件,依赖 std 命名空间:
#include <iostream>
#include <map> // map 核心头文件
#include <string>
using namespace std;
2. 初始化(常见方式)
// 方式1:空 map(默认键升序)
map<int, string> m1;
// 方式2:初始化列表(C++11+)
map<int, string> m2 = {{1, "Alice"}, {2, "Bob"}, {3, "Charlie"}};
// 方式3:迭代器范围初始化
map<int, string> m3(m2.begin(), m2.end());
// 方式4:自定义排序(键降序)
map<int, string, greater<int>> m4 = {{1, "A"}, {2, "B"}}; // 键降序:2→1
3. 核心操作(增删改查)
(1)插入键值对(insert)
insert 是最安全的插入方式,返回 pair<iterator, bool>:
first:指向插入 / 已存在键值对的迭代器;second:true(插入成功)/false(键已存在,插入失败)。
map<int, string> m;
// 方式1:插入 pair 对象
m.insert(pair<int, string>(1, "Apple"));
// 方式2:插入 make_pair(简化版)
m.insert(make_pair(2, "Banana"));
// 方式3:C++11 列表插入
m.insert({3, "Cherry"});
// 方式4:插入并判断是否成功
auto res = m.insert({2, "Blueberry"}); // 键2已存在,插入失败
if (!res.second) {
cout << "键2已存在,值为:" << res.first->second << endl; // 输出:Banana
}
2)访问 / 修改值([] 运算符 vs at ())
| 方式 | 语法 | 特点 |
|---|---|---|
[] 运算符 | m[key] | 键存在 → 返回值的引用;键不存在 → 自动插入该键,值为默认构造值(如 string 为空) |
at() 方法 | m.at(key) | 键存在 → 返回值的引用;键不存在 → 抛出 out_of_range 异常(更安全) |
map<int, string> m = {{1, "A"}, {2, "B"}};
// [] 运算符:访问存在的键
m[1] = "Apple"; // 修改值为 Apple
cout << m[1] << endl; // 输出:Apple
// [] 运算符:访问不存在的键(自动插入)
cout << m[3] << endl; // 输出空字符串,m 新增键3,值为空 string
// at() 方法:访问存在的键
m.at(2) = "Banana";
cout << m.at(2) << endl; // 输出:Banana
// at() 方法:访问不存在的键(抛异常)
// m.at(4); // 运行时抛出 std::out_of_range 异常
3)查找键(find)
- 找到:返回指向该键值对的迭代器;
- 未找到:返回
m.end()(尾后迭代器,不指向任何元素)。
map<int, string> m = {{1, "A"}, {2, "B"}, {3, "C"}};
// 查找键2
auto it = m.find(2);
if (it != m.end()) {
cout << "键2的值:" << it->second << endl; // 输出:B
// 迭代器访问键值对:it->first 是键,it->second 是值
}
// 查找不存在的键4
it = m.find(4);
if (it == m.end()) {
cout << "键4不存在" << endl;
}
4)删除键值对(erase)
支持三种删除方式,需注意迭代器失效问题:
map<int, string> m = {{1, "A"}, {2, "B"}, {3, "C"}, {4, "D"}};
// 方式1:按键删除(返回删除的个数,map 中只能是 0 或 1)
int cnt = m.erase(3); // cnt=1,m 移除键3
cout << "删除键3的个数:" << cnt << endl;
// 方式2:按迭代器删除(删除单个键值对)
auto it = m.find(2);
if (it != m.end()) {
m.erase(it); // 移除键2,m={1:A,4:D}
}
// 方式3:按迭代器范围删除(删除 [first, last) 区间)
m.erase(m.begin(), m.find(4)); // 删除键1,m={4:D}
(5)遍历 map
因无下标索引(键不一定是连续整数),只能通过迭代器或范围 for 遍历:
map<int, string> m = {{1, "A"}, {2, "B"}, {3, "C"}};
// 方式1:普通迭代器(升序)
for (map<int, string>::iterator it = m.begin(); it != m.end(); ++it) {
cout << it->first << ":" << it->second << " "; // 输出:1:A 2:B 3:C
}
cout << endl;
// 方式2:范围for(C++11+,简化版)
for (auto& pair : m) { // 用引用避免拷贝,提升效率
cout << pair.first << ":" << pair.second << " ";
}
cout << endl;
// 方式3:反向迭代器(降序)
for (map<int, string>::reverse_iterator it = m.rbegin(); it != m.rend(); ++it) {
cout << it->first << ":" << it->second << " "; // 输出:3:C 2:B 1:A
}
cout << endl;
(6)其他常用函数
map<int, string> m = {{1, "A"}, {2, "B"}};
m.size(); // 返回键值对个数:2
m.empty(); // 判断是否为空:false
m.clear(); // 清空所有键值对,m 变为空
m.count(2); // 统计键的个数(map 中只能是 0 或 1):1
m.lower_bound(2); // 返回第一个 ≥2 的键值对迭代器(指向键2)
m.upper_bound(2); // 返回第一个 >2 的键值对迭代器(指向 m.end())
四、进阶用法
1. 自定义排序规则
默认 map 按键的 < 升序排列,可通过「函数对象」或「lambda 表达式(C++11+)」修改排序规则:
(1)键为基础类型(如 int):降序排列
// 方式1:使用 STL 自带的 greater
map<int, string, greater<int>> m = {{1, "A"}, {2, "B"}, {3, "C"}};
for (auto& p : m) {
cout << p.first << ":" << p.second << " "; // 输出:3:C 2:B 1:A
}
// 方式2:自定义函数对象
struct MyCompare {
bool operator()(int a, int b) const {
return a > b; // 降序:a>b 时,a 排在 b 前面
}
};
map<int, string, MyCompare> m2 = {{1, "A"}, {2, "B"}}; // 同样降序
(2)键为自定义结构体(需显式定义排序规则)
当键是自定义结构体时,必须定义排序规则(编译器无法比较结构体大小):
// 定义结构体:学生学号+姓名(以学号为键)
struct Student {
int id; // 学号(排序依据)
string name;
// 构造函数
Student(int i, string n) : id(i), name(n) {}
};
// 自定义排序规则:按学号降序
struct CompStudent {
bool operator()(const Student& a, const Student& b) const {
return a.id > b.id; // 按学号降序排列
}
};
int main() {
// map 的键是 Student,值是分数
map<Student, int, CompStudent> m;
m.insert({Student(101, "Alice"), 90});
m.insert({Student(102, "Bob"), 85});
m.insert({Student(103, "Charlie"), 95});
// 遍历:按学号降序(103→102→101)
for (auto& p : m) {
cout << p.first.id << ":" << p.first.name << " → " << p.second << endl;
}
return 0;
}
2. map 的变体容器
(1)multimap:允许重复键的 map
multimap 与 map 几乎一致,核心区别是允许键重复:
insert始终成功(无返回bool的重载);count(key)可返回键为key的键值对个数;erase(key)会删除所有键为key的键值对;- 底层仍为红黑树,操作复杂度
O(logn)。
示例:
#include <map>
multimap<int, string> mm;
mm.insert({1, "A"});
mm.insert({1, "B"}); // 允许重复键1
mm.insert({2, "C"});
cout << mm.count(1) << endl; // 输出:2(键1有2个值)
// 遍历键1的所有值
auto range = mm.equal_range(1); // 返回 pair<iterator, iterator>,表示键1的范围
for (auto it = range.first; it != range.second; ++it) {
cout << it->first << ":" << it->second << " "; // 输出:1:A 1:B
}
(2)unordered_map:无序键值对容器
unordered_map 是「哈希表」实现的无序容器,核心特点:
- 无序性(不按键排序,存储顺序随机);
- 键唯一(无重复);
- 查找 / 插入 / 删除平均
O(1)复杂度(哈希冲突时退化为O(n)); - 不支持自定义排序(因无序),仅支持默认哈希规则(可自定义哈希函数)。
map vs unordered_map 对比:
| 特性 | map | unordered_map |
|---|---|---|
| 底层实现 | 红黑树 | 哈希表 |
| 有序性 | 按键升序(可自定义) | 无序 |
| 查找复杂度 | O(logn) | 平均 O (1),最坏 O (n) |
| 迭代器 | 双向迭代器 | 正向迭代器 |
| 适用场景 | 有序键值对、范围查询 | 高频查找、无需排序 |
示例:
#include <unordered_map>
unordered_map<int, string> um = {{1, "A"}, {2, "B"}, {3, "C"}};
for (auto& p : um) {
cout << p.first << ":" << p.second << " "; // 输出顺序随机,如 2:B 1:A 3:C
}
cout << um.find(2)->second << endl; // 输出:B(快速查找)
五、使用注意事项
1. 键的 const 性
map 的键是 const 类型(pair<const Key, T>),无法通过迭代器修改键:
map<int, string> m = {{1, "A"}};
auto it = m.find(1);
// it->first = 2; // 编译报错!键是 const,不能修改
it->second = "Apple"; // 允许修改值
若需修改键,只能先删除原键值对,再插入新的:
// 修改键1为2
int val = m[1];
m.erase(1);
m.insert({2, val});
2. [] 运算符的坑
m[key] 若键不存在,会自动插入该键并初始化值(如 int 默认为 0,string 默认为空),可能导致意外的键值对插入:
map<int, string> m;
if (m[10] == "test") { // 键10不存在,自动插入 m[10] = ""
// ...
}
cout << m.size() << endl; // 输出:1(意外新增了键10)
避免方式:先通过 find 检查键是否存在,再访问值。
3. 迭代器稳定性
map 插入 / 删除元素后,除被删除元素的迭代器外,其他迭代器均不失效(红黑树仅调整指针,未移动节点内存)。
4. 性能对比
- 若仅需「键值对映射」且无需有序,优先用
unordered_map(查找更快); - 若需「有序键值对」或「范围查询」(如找键在 [2,5) 之间的元素),用
map; - 若需「允许重复键」,用
multimap。
5. 自定义键的要求
- 作为
map/multimap的键:必须提供排序规则(函数对象),且规则满足「严格弱序」; - 作为
unordered_map的键:必须提供哈希函数和相等比较函数(默认仅支持基础类型,自定义类型需手动实现)。
六、典型应用场景
- 键值对映射:如学号→成绩、用户名→密码、ID→商品信息;
- 有序键值对查询:如按学号升序查询学生成绩、按时间戳排序的日志;
- 范围查询:结合
lower_bound()/upper_bound()找键在指定区间的键值对(如找价格在 [100, 200) 之间的商品); - 替代手动红黑树:无需自己维护平衡二叉树,直接用
map实现有序键值对管理。
七、核心总结
| 容器类型 | 核心特点 | 适用场景 |
|---|---|---|
| map | 有序、键唯一、O (logn) | 有序键值对映射、范围查询 |
| multimap | 有序、键可重复、O (logn) | 有序且允许重复键的映射 |
| unordered_map | 无序、键唯一、O (1) | 高频查找、无需排序的键值对 |
map 是 C++ 处理「有序键值对」的核心容器,掌握其特性和变体的区别,能大幅简化键值对映射的开发工作,避免重复实现红黑树或哈希表。