C++ map 容器

85 阅读9分钟

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 的映射

  1. 插入键值对时,红黑树按 Key 的大小排序,相同 Key 会被拒绝(insert 返回 false);
  2. 查找操作通过 Key 遍历红黑树,时间复杂度 O(logn)
  3. 迭代器遍历 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:指向插入 / 已存在键值对的迭代器;
  • secondtrue(插入成功)/ 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 对比:

特性mapunordered_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 的键:必须提供哈希函数和相等比较函数(默认仅支持基础类型,自定义类型需手动实现)。

六、典型应用场景

  1. 键值对映射:如学号→成绩、用户名→密码、ID→商品信息;
  2. 有序键值对查询:如按学号升序查询学生成绩、按时间戳排序的日志;
  3. 范围查询:结合 lower_bound()/upper_bound() 找键在指定区间的键值对(如找价格在 [100, 200) 之间的商品);
  4. 替代手动红黑树:无需自己维护平衡二叉树,直接用 map 实现有序键值对管理。

七、核心总结

容器类型核心特点适用场景
map有序、键唯一、O (logn)有序键值对映射、范围查询
multimap有序、键可重复、O (logn)有序且允许重复键的映射
unordered_map无序、键唯一、O (1)高频查找、无需排序的键值对

map 是 C++ 处理「有序键值对」的核心容器,掌握其特性和变体的区别,能大幅简化键值对映射的开发工作,避免重复实现红黑树或哈希表。