C++ multimap 全面解析与实战指南
在C++标准模板库(STL)的关联容器中,multimap是一种支持"一对多"映射关系的有序容器。它与map的核心区别在于允许键(key)重复,这使得它在处理需要多值映射的场景(如索引、分组等)时极具优势。本文将从multimap的底层实现原理出发,详细讲解其常用接口、核心特性,结合实战案例演示具体用法,并对比map说明适用场景,帮助大家彻底掌握这一实用容器。
一、multimap 核心原理与特性
要理解multimap的行为,首先需要明确其底层实现和核心特性。multimap与map、set、multiset同属关联容器,底层均基于红黑树(一种自平衡的二叉搜索树)实现。这种数据结构确保了容器内元素的有序性(默认按key升序排列),同时保证了插入、删除、查找等操作的时间复杂度为O(log n)。
1.1 核心特性总结
-
键值对(key-value)存储,支持键重复(一个key可对应多个value);
-
元素默认按key的升序排列(可通过自定义比较函数修改排序规则);
-
底层红黑树实现,插入、删除、查找操作效率稳定(O(log n));
-
不支持通过key直接修改value(key是排序的依据,修改会破坏有序性);
-
不支持operator[]操作符(因key重复,无法唯一定位元素);
-
迭代器为双向迭代器,支持遍历,但不支持随机访问。
1.2 与map的核心区别
很多开发者会混淆map和multimap,两者底层实现完全一致,核心差异仅在于对key唯一性的要求:
| 特性 | map | multimap |
|---|---|---|
| key唯一性 | key不可重复,插入重复key会失败 | key可重复,支持一个key对应多个value |
| operator[] | 支持,可通过key直接访问/插入元素 | 不支持,因key重复无法唯一定位 |
| 插入返回值 | 返回pair<iterator, bool>,标记插入成功与否 | 仅返回插入位置的迭代器(插入必成功) |
| 查找功能 | find()返回唯一匹配的元素迭代器 | find()返回第一个匹配key的元素迭代器,需结合equal_range()获取所有匹配元素 |
二、C++ multimap 常用接口详解
使用multimap前,需包含头文件 <map>(与map共用头文件),并使用std命名空间(或显式指定std::multimap)。multimap的接口与map大部分一致,以下重点讲解核心接口及独有的多值处理接口。
2.1 构造与析构
| 接口原型 | 功能说明 | 示例 |
|---|---|---|
| multimap(); | 默认构造函数,创建空multimap(默认key升序) | std::multimap<int, std::string> mm; |
| multimap(const Compare& comp); | 自定义比较函数构造空multimap(如降序) | std::multimap<int, std::string, std::greater> mm; |
| multimap(InputIterator first, InputIterator last); | 迭代器构造,拷贝[first, last)区间的键值对 | std::multimap<int, std::string> mm2(mm.begin(), mm.end()); |
| multimap(const multimap& other); | 拷贝构造函数 | std::multimap<int, std::string> mm3(mm); |
| ~multimap(); | 析构函数,释放所有资源(红黑树节点) | - |
2.2 迭代器相关
multimap的迭代器为双向迭代器,支持++/--操作,可用于遍历容器内的有序元素。需要注意:修改迭代器指向的元素时,不能修改key(会破坏红黑树的有序性),但可以修改value。
| 接口 | 功能说明 |
|---|---|
| begin() / end() | 返回指向第一个元素/最后一个元素下一个位置的迭代器(非const) |
| rbegin() / rend() | 返回指向最后一个元素/第一个元素前一个位置的反向迭代器 |
| cbegin() / cend() | 返回const迭代器,不可修改元素(key和value均不可改) |
| 迭代器遍历示例: |
cpp
#include <map>
#include <iostream>
#include <string>
using namespace std;
int main() {
// 构造multimap,key为int,value为string,默认升序
multimap<int, string> mm = {{1, "苹果"}, {2, "香蕉"}, {1, "橙子"}, {3, "葡萄"}};
// 正向遍历(按key升序)
cout << "正向遍历:" << endl;
for (auto it = mm.begin(); it != mm.end(); ++it) {
// it指向pair<const int, string>,key为const不可改
cout << "key: " << it->first << ", value: " << it->second << endl;
}
// 输出:
// key: 1, value: 苹果
// key: 1, value: 橙子
// key: 2, value: 香蕉
// key: 3, value: 葡萄
// 反向遍历(按key降序)
cout << "\n反向遍历:" << endl;
for (auto it = mm.rbegin(); it != mm.rend(); ++it) {
cout << "key: " << it->first << ", value: " << it->second << endl;
}
return 0;
}
2.3 容量相关
| 接口 | 功能说明 |
|---|---|
| size() | 返回当前键值对的个数 |
| empty() | 判断容器是否为空(空返回true) |
| max_size() | 返回容器可容纳的最大键值对个数(受系统内存限制) |
2.4 插入与删除(核心操作)
multimap支持多种插入方式,且因key可重复,插入操作始终成功(只要内存足够)。删除操作可按key删除、按迭代器删除,灵活性较高。
| 接口 | 功能说明 | 时间复杂度 |
|---|---|---|
| insert(const value_type& val) | 插入键值对val,返回插入位置的迭代器 | O(log n) |
| insert(InputIterator first, InputIterator last) | 插入[first, last)区间的键值对 | O(k log(n+k))(k为区间元素个数) |
| emplace(Args&&... args) | 直接构造键值对(避免拷贝,效率更高) | O(log n) |
| erase(iterator pos) | 删除迭代器pos指向的键值对,返回下一个元素的迭代器 | O(log n) |
| erase(const key_type& key) | 删除所有key匹配的键值对,返回删除的元素个数 | O(log n + k)(k为匹配的元素个数) |
| erase(iterator first, iterator last) | 删除[first, last)区间的键值对,返回下一个元素的迭代器 | O(log n + k)(k为区间元素个数) |
| clear() | 清空容器,删除所有键值对(size变为0) | O(n) |
| 插入与删除示例: |
cpp
#include <map>
#include <iostream>
#include <string>
using namespace std;
int main() {
multimap<int, string> mm;
// 1. 插入单个键值对
mm.insert({1, "苹果"}); // 用初始化列表
mm.insert(pair<int, string>(1, "橙子")); // 用pair构造
mm.emplace(2, "香蕉"); // emplace直接构造(更高效)
cout << "插入后size: " << mm.size() << endl; // 输出:3
// 2. 按key删除(删除所有key=1的元素)
size_t delCount = mm.erase(1);
cout << "删除key=1的元素个数: " << delCount << endl; // 输出:2
cout << "删除后size: " << mm.size() << endl; // 输出:1
// 3. 按迭代器删除
auto it = mm.begin();
mm.erase(it);
cout << "按迭代器删除后size: " << mm.size() << endl; // 输出:0
// 4. 清空容器
mm.insert({3, "葡萄"}, {4, "芒果"});
mm.clear();
cout << "clear后empty: " << mm.empty() << endl; // 输出:1(true)
return 0;
}
2.5 查找与统计(核心操作)
由于multimap支持key重复,查找操作除了常规的find(),还提供了专门用于获取同一key所有对应元素的接口(equal_range()、lower_bound()、upper_bound()),这是multimap的核心优势所在。
| 接口 | 功能说明 | 返回值 |
|---|---|---|
| find(const key_type& key) | 查找key匹配的第一个元素 | 匹配元素的迭代器;若无匹配,返回end() |
| count(const key_type& key) | 统计key匹配的元素个数 | 匹配元素的个数(无匹配返回0) |
| lower_bound(const key_type& key) | 查找第一个key >= 目标key的元素 | 对应迭代器;若无则返回end() |
| upper_bound(const key_type& key) | 查找第一个key > 目标key的元素 | 对应迭代器;若无则返回end() |
| equal_range(const key_type& key) | 获取所有key == 目标key的元素区间 | pair<iterator, iterator>,first为lower_bound()结果,second为upper_bound()结果 |
| 查找与统计示例(核心重点): |
cpp
#include <map>
#include <iostream>
#include <string>
using namespace std;
int main() {
multimap<int, string> mm = {{1, "苹果"}, {2, "香蕉"}, {1, "橙子"}, {3, "葡萄"}, {1, "草莓"}};
// 1. 查找key=1的第一个元素
auto findIt = mm.find(1);
if (findIt != mm.end()) {
cout << "find key=1: " << findIt->first << " - " << findIt->second << endl;
} // 输出:find key=1: 1 - 苹果
// 2. 统计key=1的元素个数
size_t count = mm.count(1);
cout << "key=1的元素个数: " << count << endl; // 输出:3
// 3. 用lower_bound和upper_bound获取key=1的所有元素
auto lowIt = mm.lower_bound(1);
auto upIt = mm.upper_bound(1);
cout << "\nlower_bound & upper_bound遍历key=1的元素:" << endl;
for (auto it = lowIt; it != upIt; ++it) {
cout << it->first << " - " << it->second << endl;
}
// 输出:
// 1 - 苹果
// 1 - 橙子
// 1 - 草莓
// 4. 用equal_range获取key=1的所有元素(更简洁)
auto range = mm.equal_range(1);
cout << "\nequal_range遍历key=1的元素:" << endl;
for (auto it = range.first; it != range.second; ++it) {
cout << it->first << " - " << it->second << endl;
} // 输出与上面一致
// 5. 查找不存在的key
auto noIt = mm.find(4);
if (noIt == mm.end()) {
cout << "\nkey=4不存在" << endl;
}
return 0;
}
三、multimap 实战案例
multimap的核心应用场景是"多值映射",以下通过两个经典案例演示其实际用法:
3.1 场景1:学生成绩分组(按班级索引)
需求:存储多个班级学生的成绩,按班级号分组,支持查询某个班级的所有学生成绩。
cpp
#include <map>
#include <iostream>
#include <string>
using namespace std;
// 存储学生信息:key=班级号,value=学生姓名+成绩(用pair封装)
using StuMap = multimap<int, pair<string, int>>;
// 添加学生成绩
void addStudent(StuMap& mm, int classId, const string& name, int score) {
mm.emplace(classId, make_pair(name, score));
}
// 查询某个班级的所有学生成绩
void queryClassScore(const StuMap& mm, int classId) {
auto range = mm.equal_range(classId);
if (range.first == range.second) {
cout << "班级" << classId << "无学生信息" << endl;
return;
}
cout << "\n班级" << classId << "学生成绩:" << endl;
for (auto it = range.first; it != range.second; ++it) {
cout << "姓名:" << it->second.first << ",成绩:" << it->second.second << endl;
}
}
int main() {
StuMap mm;
// 添加学生数据
addStudent(mm, 1, "张三", 95);
addStudent(mm, 1, "李四", 88);
addStudent(mm, 2, "王五", 92);
addStudent(mm, 2, "赵六", 79);
addStudent(mm, 1, "孙七", 90);
// 查询班级1成绩
queryClassScore(mm, 1);
// 查询班级2成绩
queryClassScore(mm, 2);
// 查询不存在的班级3
queryClassScore(mm, 3);
return 0;
}
输出结果:
text
班级1学生成绩:
姓名:张三,成绩:95
姓名:李四,成绩:88
姓名:孙七,成绩:90
班级2学生成绩:
姓名:王五,成绩:92
姓名:赵六,成绩:79
班级3无学生信息
3.2 场景2:单词索引(按单词出现的行号映射)
需求:读取一段文本,记录每个单词出现的所有行号,支持查询某个单词的所有出现位置。
cpp
#include <map>
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
// 单词索引:key=单词,value=出现的行号
using WordIndex = multimap<string, int>;
// 构建单词索引(忽略大小写,简单处理标点)
void buildWordIndex(WordIndex& mm, const string& text) {
stringstream ss(text);
string line;
int lineNum = 0;
// 按行读取文本
while (getline(ss, line)) {
lineNum++;
stringstream lineSs(line);
string word;
// 按空格分割单词
while (lineSs >> word) {
// 简单处理:移除单词末尾的标点(逗号、句号、分号)
if (!word.empty() && ispunct(word.back())) {
word.pop_back();
}
// 转换为小写(统一索引)
for (char& c : word) {
c = tolower(c);
}
// 插入索引
mm.emplace(word, lineNum);
}
}
}
// 查询单词出现的所有行号
void queryWord(const WordIndex& mm, const string& word) {
string lowerWord = word;
for (char& c : lowerWord) {
c = tolower(c);
}
auto range = mm.equal_range(lowerWord);
if (range.first == range.second) {
cout << "单词\"" << word << "\"未出现" << endl;
return;
}
cout << "单词\"" << word << "\"出现的行号:";
for (auto it = range.first; it != range.second; ++it) {
cout << it->second << " ";
}
cout << endl;
}
int main() {
// 测试文本
string text = R"(Hello world!
Hello C++.
World is beautiful.
C++ is fun.)";
WordIndex mm;
buildWordIndex(mm, text);
// 查询单词
queryWord(mm, "hello");
queryWord(mm, "C++");
queryWord(mm, "world");
queryWord(mm, "java");
return 0;
}
输出结果:
text
单词"hello"出现的行号:1 2
单词"C++"出现的行号:2 4
单词"world"出现的行号:1 3
单词"java"未出现
四、multimap 使用注意事项
-
禁止修改key值:multimap的key是排序的依据,修改key会破坏红黑树的有序性,导致容器行为异常。若需修改key,需先删除原键值对,再插入新的键值对。
-
不支持operator[]:因key可重复,无法通过key唯一定位元素,因此multimap没有实现operator[]接口,需通过find()、equal_range()等接口访问元素。
-
自定义排序规则:默认按key升序排列,可通过自定义比较函数修改排序(如降序)。自定义比较函数需满足"严格弱序"(即不可传递的等价关系)。
-
迭代器失效问题:插入元素时,红黑树可能会旋转调整,但迭代器不会失效(除了指向被删除元素的迭代器);删除元素时,只有指向被删除元素的迭代器失效,其他迭代器仍有效。
-
效率考量:若无需key重复,优先使用map(map的查找、插入效率略高于multimap,因无需处理重复key);若需要频繁按key分组查询,multimap是最优选择。
-
线程安全性:与所有STL容器一致,multimap不保证线程安全,多线程环境下并发读写需手动加锁(如使用std::mutex)。
五、总结
multimap是C++ STL中用于处理"一对多"映射关系的有序容器,底层基于红黑树实现,确保了元素有序性和高效的插入、删除、查找操作。其核心优势在于支持key重复,通过equal_range()等接口可便捷地获取同一key对应的所有value,特别适合分组、索引等场景。
使用时需注意:禁止修改key值、不支持operator[]接口,若无需key重复应优先选择map。掌握multimap的核心接口(尤其是equal_range())和适用场景,能帮助我们在处理多值映射问题时写出更简洁、高效的代码。