关联式容器:map 与 multimap 的键值对存储
在上一篇博客中,我们讲解了set与multiset的有序存储特性------它们以"元素本身作为关键字",核心解决"有序去重"和"有序可重复"的存储需求。而今天要介绍的map与multimap,同样是C++ STL中常用的有序关联式容器,却有着本质区别:它们以"键值对(key-value)"为存储单元,关键字(key)与值(value)分离,专门解决"通过关键字快速查找对应值"的场景。
很多开发者在初学时,容易混淆map与multimap,也常把它们和set系列弄混------比如"误以为map的value是关键字""想用map存储重复关键字却发现失败""不知道multimap如何高效获取重复key对应的所有value"。更有甚者,在需要键值对存储时,盲目用vector存储结构体模拟键值对,忽略了map/multimap自带的有序性和高效查找特性,导致代码冗余、性能低下。
今天这篇博客,我们聚焦map与multimap的核心------键值对存储,延续上一篇的讲解逻辑,从底层原理简化解析、核心特性对比、基础用法、进阶实操,到适用场景与避坑指南,用通俗的讲解+可直接运行的示例代码,帮你彻底吃透这两个容器,搞懂"它们如何存储键值对""什么时候用map、什么时候用multimap""怎么高效运用它们的键值对有序特性"。
一、先搞懂核心:map 与 multimap 是什么?
map与multimap同属于STL中的有序关联式容器,和set/multiset一样,底层同样基于红黑树(自平衡二叉搜索树)实现。它们的核心功能都是"按关键字(key)有序存储键值对(key-value)",区别仅在于"是否允许存储重复的关键字(key)"------这也是我们区分两者的关键,没有其他本质差异。
这里先明确一个核心区别(与set系列对比):
-
set/multiset:存储"单一值",元素本身就是关键字,核心是"有序存储元素";
-
map/multimap:存储"键值对",关键字(key)与值(value)分离,核心是"通过key有序查找value"。
1. 核心定义(精准不冗余)
-
map(映射):存储唯一关键字(key)的键值对,自动按关键字升序(默认)排序,不允许重复key。若插入重复key,会被自动忽略,不会报错;每个key唯一对应一个value,实现"一对一"映射。
-
multimap(多重映射):存储可重复关键字(key)的键值对,自动按关键字升序(默认)排序,允许重复key。若插入重复key,会全部保留,且按排序规则插入到对应位置,维持整体有序;一个key可对应多个value,实现"一对多"映射。
一句话总结:两者都按key有序存储键值对,唯一区别是map的key唯一(一对一),multimap的key可重复(一对多)。
2. 底层原理简化解析(复用红黑树逻辑,重点讲键值对特性)
map与multimap的底层实现和set/multiset一致,均基于红黑树,因此它们继承了红黑树的核心优势:自动排序、高效操作(插入、删除、查找O(log n))。但由于存储的是键值对,红黑树的节点结构有所不同------每个节点存储一个pair<const key_type, value_type>类型的键值对,其中:
-
key是const修饰的:不可修改,因为key是红黑树排序的依据,修改key会破坏红黑树的有序性,导致容器失效;
-
value是可修改的:value仅用于存储数据,与红黑树的排序无关,修改value不会影响容器的有序性;
-
排序规则:红黑树仅根据key进行排序,与value无关,默认按key升序排列,也可自定义key的排序规则。
补充两个关键细节(与set系列对比):
-
去重机制(仅map):map的去重仅针对key,与value无关------即使两个键值对的value不同,只要key相同,插入时就会被忽略;
-
重复存储(仅multimap):multimap的重复仅针对key,多个相同key对应的value可以不同,且会按插入顺序(或排序规则)相邻存储。
3. map vs multimap 核心特性对比表(一目了然)
为了快速区分两者,避免用错,同时对比set系列的核心差异,这里整理了核心特性对比(聚焦键值对存储、key唯一性、操作效率等关键维度):
| 特性 | map(映射) | multimap(多重映射) | 补充(与set对比) |
|---|---|---|---|
| 存储单元 | 键值对(key-value),pair类型 | 键值对(key-value),pair类型 | set存储单一元素(元素=key) |
| 有序性 | 按key自动升序(默认),维持有序 | 按key自动升序(默认),维持有序 | set按元素本身(key)有序 |
| 重复key | 不允许,插入重复key会被忽略 | 允许,重复key全部保留,按序排列 | set不允许重复元素(key) |
| key特性 | key唯一、不可修改 | key可重复、不可修改 | set元素(key)不可修改 |
| value特性 | 可修改,与排序无关 | 可修改,与排序无关 | set无value,仅存储key |
| 映射关系 | 一对一(一个key对应一个value) | 一对多(一个key对应多个value) | set无映射关系,仅存储key |
| 插入/删除/查找效率 | O(log n)(基于红黑树,按key操作) | O(log n)(基于红黑树,与map一致) | set效率一致,按元素(key)操作 |
| 迭代器特性 | 双向迭代器,解引用返回pair对象 | 双向迭代器,与map完全一致 | set迭代器解引用返回单一元素 |
| 核心用途 | 通过唯一key快速查找value(一对一映射) | 通过重复key获取多个value(一对多映射) | 有序去重存储、高效查找元素 |
| 关键提醒1:map和multimap中的key不可修改,但value可修改!修改key会破坏红黑树的有序性,修改value不影响有序性。 |
关键提醒2:map的[]运算符是其特有用法(multimap不支持),可快速插入/访问键值对,但需注意使用陷阱(后续避坑部分讲解)。
二、基础用法:从初始化到核心操作(附示例,可直接复制)
使用map与multimap前,需包含头文件 #include <map>,并使用std命名空间(或显式调用std::map、std::multimap)。由于两者用法高度一致,我们统一讲解,重点标注差异点(如map的[]运算符、multimap的重复key操作)。
核心前提:map与multimap存储的键值对是std::pair类型,pair<const K, V>,其中K是key的类型,V是value的类型。可通过pair.first访问key,pair.second访问value。
1. 初始化(5种常用方式,map和multimap通用,差异标注)
初始化时可指定key的排序规则(默认升序),以下示例默认升序,自定义排序后续单独讲解。注意:multimap不支持用[]运算符初始化。
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
// 方式1:默认初始化,空容器,默认key升序
map<int, string> m1;
multimap<int, string> mm1;
// 方式2:初始化时插入多个键值对(C++11及以上,初始化列表,最简洁)
map<int, string> m2 = {{1, "张三"}, {3, "李四"}, {2, "王五"}, {1, "赵六"}};
// 自动按key去重+排序,最终m2: {1:"张三", 2:"王五", 3:"李四"}(重复key=1被忽略)
multimap<int, string> mm2 = {{1, "张三"}, {3, "李四"}, {2, "王五"}, {1, "赵六"}};
// 允许重复key+按key排序,最终mm2: {1:"张三", 1:"赵六", 2:"王五", 3:"李四"}
// 方式3:通过pair对象插入初始化(兼容所有C++版本)
map<int, string> m3;
m3.insert(pair<int, string>(4, "孙七"));
m3.insert(make_pair(5, "周八")); // make_pair更简洁,推荐
multimap<int, string> mm3;
mm3.insert(make_pair(1, "吴九"));
mm3.insert(make_pair(1, "郑十"));
// 方式4:拷贝构造(从另一个map/multimap拷贝)
map<int, string> m4(m2); // m4与m2完全一致:{1:"张三", 2:"王五", 3:"李四"}
multimap<int, string> mm4(mm2); // mm4与mm2完全一致:{1:"张三", 1:"赵六", 2:"王五", 3:"李四"}
// 方式5:指定排序规则初始化(示例:key降序,后续详解)
map<int, string, greater<int>> m5 = {{1, "张三"}, {3, "李四"}, {2, "王五"}};
// 降序+去重:{3:"李四", 2:"王五", 1:"张三"}
multimap<int, string, greater<int>> mm5 = {{1, "张三"}, {3, "李四"}, {2, "王五"}, {1, "赵六"}};
// 降序+重复:{3:"李四", 2:"王五", 1:"张三", 1:"赵六"}
// 打印测试(后续讲遍历方式,此处暂用简单遍历)
cout << "m2: ";
for (auto it = m2.begin(); it != m2.end(); ++it) {
// it是迭代器,解引用返回pair对象,first=key,second=value
cout << "(" << it->first << "," << it->second << ") ";
}
cout << endl; // 输出:(1,张三) (2,王五) (3,李四)
cout << "mm2: ";
for (auto it = mm2.begin(); it != mm2.end(); ++it) {
cout << "(" << it->first << "," << it->second << ") ";
}
cout << endl; // 输出:(1,张三) (1,赵六) (2,王五) (3,李四)
return 0;
}
2. 核心操作(高频必学,区分map与multimap差异)
map与multimap的大部分操作完全一致,差异主要集中在"插入重复key""访问value""删除重复key"上,重点标注差异点,同时讲解map特有的[]运算符。
(1)插入元素:insert()(核心,体现键值对+key唯一性特性)
插入元素时,容器会自动按key排序并(map)去重,时间复杂度O(log n),支持插入单个键值对、多个键值对、区间键值对,插入的是pair类型对象。
-
map:插入重复key,返回"插入失败"的标识(pair类型,first是迭代器,指向当前key对应的pair;second是bool值,false表示插入失败);
-
multimap:插入重复key,直接成功,返回指向插入的pair对象的迭代器,无bool标识。
cpp
map<int, string> m = {{1, "张三"}, {2, "王五"}};
multimap<int, string> mm = {{1, "张三"}, {2, "王五"}};
// 1. 插入单个键值对(两种方式:make_pair和pair构造)
// map插入重复key=1(已存在),插入失败
auto res1 = m.insert(make_pair(1, "赵六"));
cout << "map插入key=1:" << (res1.second ? "成功" : "失败") << endl; // 输出:失败
// map插入新key=3,插入成功
auto res2 = m.insert(pair<int, string>(3, "李四"));
cout << "map插入key=3:" << (res2.second ? "成功" : "失败") << endl; // 输出:成功,m变为{1:"张三", 2:"王五", 3:"李四"}
// multimap插入重复key=1(已存在),插入成功
auto it_mm = mm.insert(make_pair(1, "赵六"));
cout << "multimap插入key=1,插入位置键值对:(" << it_mm->first << "," << it_mm->second << ")" << endl; // 输出:(1,赵六)
// multimap插入新key=3,插入成功
mm.insert(make_pair(3, "李四")); // mm变为{1:"张三", 1:"赵六", 2:"王五", 3:"李四"}
// 2. 插入多个键值对(初始化列表)
m.insert({{4, "孙七"}, {5, "周八"}, {2, "吴九"}}); // 去重+按key排序,m变为{1:"张三", 2:"王五", 3:"李四", 4:"孙七", 5:"周八"}(key=2重复被忽略)
mm.insert({{4, "孙七"}, {5, "周八"}, {2, "吴九"}}); // 重复+按key排序,mm变为{1:"张三", 1:"赵六", 2:"王五", 2:"吴九", 3:"李四", 4:"孙七", 5:"周八"}
// 3. 插入区间键值对(从另一个map/multimap拷贝)
map<int, string> m_temp = {{6, "郑十"}, {7, "钱十一"}};
m.insert(m_temp.begin(), m_temp.end()); // m变为{1:"张三", 2:"王五", ..., 6:"郑十", 7:"钱十一"}
multimap<int, string> mm_temp = {{6, "郑十"}, {6, "钱十一"}};
mm.insert(mm_temp.begin(), mm_temp.end()); // mm变为{1:"张三", ..., 6:"郑十", 6:"钱十一"}
(2)map特有:[]运算符(快速插入/访问键值对,注意陷阱)
map支持[]运算符,可通过key快速访问或插入对应的value,这是map与multimap、set系列的核心区别之一(multimap、set均不支持[]运算符)。其底层逻辑是:
-
若key已存在:返回该key对应的value的引用,可直接修改value;
-
若key不存在:自动插入一个键值对(key为指定值,value为默认构造值,如string默认空串、int默认0),并返回value的引用。
cpp
map<int, string> m = {{1, "张三"}, {2, "王五"}};
// 1. 访问已存在的key,返回value引用
cout << "m[1] = " << m[1] << endl; // 输出:m[1] = 张三
// 修改已存在key的value
m[1] = "赵六";
cout << "修改后m[1] = " << m[1] << endl; // 输出:修改后m[1] = 赵六,m变为{1:"赵六", 2:"王五"}
// 2. 访问不存在的key,自动插入键值对(key=3,value为空串)
cout << "m[3] = " << m[3] << endl; // 输出:m[3] = (空串),m变为{1:"赵六", 2:"王五", 3:""}
// 给自动插入的key赋值
m[3] = "李四";
cout << "赋值后m[3] = " << m[3] << endl; // 输出:赋值后m[3] = 李四
// 3. 直接用[]插入新键值对
m[4] = "孙七"; // 插入{4:"孙七"},m变为{1:"赵六", 2:"王五", 3:"李四", 4:"孙七"}
// 关键陷阱:不要用[]判断key是否存在!
// 原因:即使key不存在,[]也会自动插入该key(value默认构造),导致误插入
if (m[5] == "") { // 此时key=5被自动插入,m的size增加1
cout << "key=5不存在" << endl;
}
cout << "m的size:" << m.size() << endl; // 输出:5(多了key=5)
// 正确判断key是否存在:用find()或count()(后续讲解)
(3)删除元素:erase()(3种常用方式,注意multiset删除重复key的细节)
删除元素时,时间复杂度O(log n),支持按迭代器删除、按key删除、按区间删除,map与multiset的差异在于"按key删除"时的行为(与set/multiset的差异逻辑一致)。
-
map:按key删除,最多删除1个元素(因为key唯一),返回删除的元素个数(0或1);
-
multimap:按key删除,会删除所有key等于该值的键值对,返回删除的元素个数。
cpp
map<int, string> m = {{1, "张三"}, {2, "王五"}, {3, "李四"}, {4, "孙七"}};
multimap<int, string> mm = {{1, "张三"}, {1, "赵六"}, {2, "王五"}, {3, "李四"}, {3, "周八"}, {3, "吴九"}};
// 方式1:按迭代器删除(删除指定位置的键值对)
auto it_m = m.begin();
++it_m; // 指向key=2的键值对(2,"王五")
m.erase(it_m); // 删除key=2,m变为{1:"张三", 3:"李四", 4:"孙七"}
auto it_mm = mm.begin();
++it_mm; // 指向第一个key=1的键值对(1,"赵六")
mm.erase(it_mm); // 删除该键值对,mm变为{1:"张三", 2:"王五", 3:"李四", 3:"周八", 3:"吴九"}
// 方式2:按key删除(核心差异点)
int del_key = 3;
// map按key删除3,删除1个(key唯一)
int m_del_cnt = m.erase(del_key);
cout << "map删除key=" << del_key << ",删除个数:" << m_del_cnt << endl; // 输出:1,m变为{1:"张三", 4:"孙七"}
// multimap按key删除3,删除所有key=3的键值对(共3个)
int mm_del_cnt = mm.erase(del_key);
cout << "multimap删除key=" << del_key << ",删除个数:" << mm_del_cnt << endl; // 输出:3,mm变为{1:"张三", 2:"王五"}
// 方式3:按区间删除(左闭右开,删除[m.begin(), m.end()-1])
auto it1 = m.begin();
auto it2 = m.end();
--it2; // 指向key=4的键值对(4,"孙七")
m.erase(it1, it2); // 删除key=1,m变为{4:"孙七"}
auto it3 = mm.begin();
auto it4 = mm.begin();
++it4; // 指向key=2的键值对(2,"王五")
mm.erase(it3, it4); // 删除key=1,mm变为{2:"王五"}
(4)查找元素:find() + count()(高效查找,核心优势)
基于红黑树的查找,时间复杂度O(log n),远优于vector模拟键值对的O(n),核心是按key查找,用法与set系列类似,但返回值和解引用结果不同(返回pair对象)。
① find():查找key,返回迭代器
-
查找成功:返回指向该key对应的键值对(pair)的迭代器;map中key唯一,仅返回一个迭代器;multimap中key可重复,返回第一个匹配key的迭代器;
-
查找失败:返回容器的end()迭代器(不可解引用)。
② count():统计key出现的次数
-
map:返回0或1(key唯一),可用于判断key是否存在;
-
multimap:返回该key对应的键值对个数(>=0),可用于获取重复key的数量。
cpp
map<int, string> m = {{1, "张三"}, {2, "王五"}, {3, "李四"}};
multimap<int, string> mm = {{1, "张三"}, {1, "赵六"}, {2, "王五"}, {3, "李四"}, {3, "周八"}};
// 1. find()查找
// map查找存在的key=2
auto it_m1 = m.find(2);
if (it_m1 != m.end()) {
cout << "map找到key=2,键值对:(" << it_m1->first << "," << it_m1->second << ")" << endl; // 输出:(2,王五)
}
// multimap查找存在的key=1(返回第一个匹配的迭代器)
auto it_mm1 = mm.find(1);
if (it_mm1 != mm.end()) {
cout << "multimap找到key=1,第一个键值对:(" << it_mm1->first << "," << it_mm1->second << ")" << endl; // 输出:(1,张三)
}
// 查找不存在的key=4
auto it_m2 = m.find(4);
if (it_m2 == m.end()) {
cout << "map未找到key=4" << endl; // 输出:未找到
}
auto it_mm2 = mm.find(4);
if (it_mm2 == mm.end()) {
cout << "multimap未找到key=4" << endl; // 输出:未找到
}
// 2. count()统计
cout << "map中key=1的个数:" << m.count(1) << endl; // 输出:1(map key唯一)
cout << "multimap中key=1的个数:" << mm.count(1) << endl; // 输出:2(两个key=1的键值对)
cout << "map中key=5的个数:" << m.count(5) << endl; // 输出:0(不存在)
// 正确判断key是否存在(推荐用find(),效率更高)
if (m.find(3) != m.end()) {
cout << "map中存在key=3" << endl;
}
if (mm.count(3) > 0) {
cout << "multimap中存在key=3" << endl;
}
(5)其他高频操作(map与multiset通用)
-
size():返回容器中键值对的个数,时间复杂度O(1); -
empty():判断容器是否为空(size() == 0),返回bool值,高效推荐; -
clear():清空容器中所有键值对,释放红黑树节点内存,size变为0; -
swap():交换两个同类型容器的键值对,时间复杂度O(1)(仅交换红黑树的根节点,无需拷贝元素); -
begin()/end():返回双向迭代器,指向容器的第一个/最后一个键值对(end()指向末尾后一个位置,不可解引用); -
rbegin()/rend():返回反向迭代器,用于逆序遍历容器。
cpp
map<int, string> m = {{1, "张三"}, {2, "王五"}};
multimap<int, string> mm = {{1, "张三"}, {1, "赵六"}};
cout << "m的大小:" << m.size() << endl; // 输出:2
cout << "mm是否为空:" << (mm.empty() ? "是" : "否") << endl; // 输出:否
// 交换容器
map<int, string> m_temp = {{3, "李四"}, {4, "孙七"}};
m.swap(m_temp);
cout << "交换后m的元素:";
for (auto val : m) { cout << "(" << val.first << "," << val.second << ") "; } // 输出:(3,李四) (4,孙七)
// 清空容器
mm.clear();
cout << "清空后mm的大小:" << mm.size() << endl; // 输出:0
// 反向遍历
cout << "m反向遍历:";
for (auto it = m.rbegin(); it != m.rend(); ++it) {
cout << "(" << it->first << "," << it->second << ") "; // 输出:(4,孙七) (3,李四)
}
cout << endl;
3. 遍历方式(3种常用,适配双向迭代器,重点看pair访问)
map与multimap的迭代器是双向迭代器 ,支持++、--操作,不支持随机访问(无法用下标[ ]访问,除了map的[]运算符用于访问value),常用3种遍历方式,两者用法完全一致。核心是:迭代器解引用返回pair<const K, V>对象,通过->first访问key,->second访问value。
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<int, string> m = {{1, "张三"}, {2, "王五"}, {3, "李四"}};
multimap<int, string> mm = {{1, "张三"}, {1, "赵六"}, {2, "王五"}, {3, "李四"}};
// 方式1:迭代器遍历(最通用,推荐)
// 普通迭代器(可读,可修改value,不可修改key)
cout << "map迭代器遍历:";
map<int, string>::iterator it_m;
for (it_m = m.begin(); it_m != m.end(); ++it_m) {
// 两种访问方式:it->first / (*it).first(推荐前者,更简洁)
cout << "(" << it_m->first << "," << it_m->second << ") ";
// 修改value(合法)
it_m->second += "_修改后";
}
cout << endl; // 输出:(1,张三) (2,王五) (3,李四)(修改后的值后续打印)
// 常量迭代器(只读,更安全,不可修改value和key)
cout << "multiset常量迭代器遍历:";
multimap<int, string>::const_iterator cit_mm;
for (cit_mm = mm.cbegin(); cit_mm != mm.cend(); ++cit_mm) {
cout << "(" << cit_mm->first << "," << cit_mm->second << ") ";
// cit_mm->second = "修改"; // 编译报错,常量迭代器不可修改value
}
cout << endl; // 输出:(1,张三) (1,赵六) (2,王五) (3,李四)
// 打印修改后的map
cout << "修改value后map:";
for (auto it = m.begin(); it != m.end(); ++it) {
cout << "(" << it->first << "," << it->second << ") "; // 输出:(1,张三_修改后) (2,王五_修改后) (3,李四_修改后)
}
cout << endl;
// 方式2:范围for循环(C++11及以上,最简洁,推荐)
cout << "map范围for遍历:";
for (auto& val : m) { // 用引用可修改value,不用引用则只读
cout << "(" << val.first << "," << val.second << ") ";
val.second = "再次修改"; // 用引用修改value(合法)
}
cout << endl;
// 方式3:反向迭代器(从尾部到头部遍历,逆序)
cout << "multimap反向遍历:";
multimap<int, string>::reverse_iterator rit_mm;
for (rit_mm = mm.rbegin(); rit_mm != mm.rend(); ++rit_mm) {
cout << "(" << rit_mm->first << "," << rit_mm->second << ") "; // 输出:(3,李四) (2,王五) (1,赵六) (1,张三)
}
cout << endl;
// 错误示例1:不支持随机访问,无法用下标访问(除了map的[]访问value)
// cout << m[0]->second << endl; // 编译报错
// auto it = m.begin() + 2; // 编译报错,只能用++、--跳转
// 错误示例2:修改key(非法)
// it_m = m.begin();
// it_m->first = 10; // 编译报错,key是const修饰的,不可修改
return 0;
}
三、进阶用法:自定义排序与自定义key/value类型
map与multimap默认按"key升序"排序,value可以是任意基本类型或自定义类型。实际开发中,我们经常需要自定义key的排序规则(如降序、按自定义结构体key排序),或存储自定义类型的value,这也是键值对存储的核心进阶场景。
核心原则:排序规则仅针对key,与value无关;自定义key类型时,必须指定排序规则(否则编译器无法比较key大小)。
1. 自定义排序规则(3种常用方式,与set系列一致)
自定义排序的核心是:向map/multimap传递"排序函数"或"函数对象",告诉容器如何比较key的大小,维持有序性。
方式1:使用STL内置函数对象(最简单,适用于内置类型key)
STL提供了greater<K>(key降序)、less<K>(key升序,默认)两个函数对象,直接作为模板参数即可(K是key的类型)。
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
// 方式1:key降序排序(内置greater函数对象)
map<int, string, greater<int>> m_desc = {{1, "张三"}, {3, "李四"}, {2, "王五"}};
// 降序+去重:{3:"李四", 2:"王五", 1:"张三"}
multimap<int, string, greater<int>> mm_desc = {{1, "张三"}, {3, "李四"}, {2, "王五"}, {1, "赵六"}};
// 降序+重复:{3:"李四", 2:"王五", 1:"张三", 1:"赵六"}
cout << "map key降序遍历:";
for (auto val : m_desc) { cout << "(" << val.first << "," << val.second << ") "; } // 输出:(3,李四) (2,王五) (1,张三)
cout << endl;
cout << "multimap key降序遍历:";
for (auto val : mm_desc) { cout << "(" << val.first << "," << val.second << ") "; } // 输出:(3,李四) (2,王五) (1,张三) (1,赵六)
cout << endl;
return 0;
}
方式2:自定义全局排序函数(适用于简单自定义规则,内置类型key)
定义一个返回bool值的全局函数,参数为两个key(K类型),返回"a是否应该排在b前面",作为模板参数传递给容器。
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
// 自定义排序规则:key按绝对值降序排序(key是int类型)
bool cmpKeyAbsDesc(int a, int b) {
return abs(a) > abs(b); // 绝对值大的key排在前面
}
int main() {
map<int, string, decltype(cmpKeyAbsDesc)*> m(cmpKeyAbsDesc); // 传递排序函数
multimap<int, string, decltype(cmpKeyAbsDesc)*> mm(cmpKeyAbsDesc);
m.insert({{-5, "张三"}, {3, "李四"}, {-2, "王五"}, {5, "赵六"}}); // 去重(key=-5和5绝对值相同,保留第一个)+ 按绝对值降序:{5:"赵六", -2:"王五", 3:"李四"}
mm.insert({{-5, "张三"}, {3, "李四"}, {-2, "王五"}, {5, "赵六"}}); // 重复(key=-5和5绝对值相同,视为不同key)+ 按绝对值降序:{-5:"张三",5:"赵六",3:"李四",-2:"王五"}
cout << "map key按绝对值降序:";
for (auto val : m) { cout << "(" << val.first << "," << val.second << ") "; }
cout << endl;
cout << "multimap key按绝对值降序:";
for (auto val : mm) { cout << "(" << val.first << "," << val.second << ") "; }
cout << endl;
return 0;
}
方式3:自定义函数对象(适用于复杂规则、自定义类型key)
定义一个结构体/类,重载()运算符(成为函数对象),在运算符中实现key的排序规则,适用于自定义类型key或复杂的排序逻辑。
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
// 自定义key类型:学生学号(结构体)
struct StudentID {
int grade; // 年级
int num; // 学号(年级内唯一)
// 构造函数(方便初始化)
StudentID(int g, int n) : grade(g), num(n) {}
};
// 自定义函数对象:按key(StudentID)排序,先按年级升序,年级相同按学号升序
struct CmpStudentID {
bool operator()(const StudentID& a, const StudentID& b) const {
if (a.grade != b.grade) {
return a.grade < b.grade; // 年级升序
} else {
return a.num < b.num; // 年级相同,学号升序
}
}
};
// 自定义value类型:学生信息(结构体)
struct StudentInfo {
string name;
int age;
StudentInfo(string n, int a) : name(n), age(a) {}
};
int main() {
// 存储自定义key(StudentID)和自定义value(StudentInfo),使用自定义函数对象排序
map<StudentID, StudentInfo, CmpStudentID> m_stu;
multimap<StudentID, StudentInfo, CmpStudentID> mm_stu;
// 插入键值对(key=StudentID,value=StudentInfo)
m_stu.insert(make_pair(StudentID(3, 101), StudentInfo("张三", 18)));
m_stu.insert(make_pair(StudentID(2, 201), StudentInfo("李四", 17)));
m_stu.insert(make_pair(StudentID(3, 102), StudentInfo("王五", 18)));
m_stu.insert(make_pair(StudentID(3, 101), StudentInfo("赵六", 18))); // key重复(3年级101号),插入失败
mm_stu.insert(make_pair(StudentID(3, 101), StudentInfo("张三", 18)));
mm_stu.insert(make_pair(StudentID(3, 101), StudentInfo("赵六", 18))); // key重复,插入成功
// 遍历输出
cout << "map学生信息(按年级+学号升序):" << endl;
for (const auto& val : m_stu) {
cout << "学号:" << val.first.grade << "年级" << val.first.num << "号,姓名:" << val.second.name << ",年龄:" << val.second.age << endl;
}
cout << "\nmultimap学生信息:" << endl;
for (const auto& val : mm_stu) {
cout << "学号:" << val.first.grade << "年级" << val.first.num << "号,姓名:" << val.second.name << ",年龄:" << val.second.age << endl;
}
return 0;
}
注意:自定义key类型时,必须指定排序规则(函数对象或全局函数);自定义value类型时,无需指定排序规则(排序与value无关),但需确保value类型支持必要的构造、赋值操作。
四、适用场景:map 与 multimap 什么时候用?(精准匹配)
结合两者的键值对存储特性和key唯一性差异,以下场景优先选择map或multimap,替代vector模拟键值对的方式,提升代码效率和简洁度。同时对比set系列的适用场景,帮你快速区分。
1. map 的适用场景(key唯一+键值对+有序)
-
一对一映射存储:需要通过唯一key快速查找对应value,且希望key有序,无需手动排序(如用户ID→用户信息、学号→学生成绩、字典(单词→释义));
-
高效查找+修改:需要频繁根据key查找value,并修改value的值(如根据用户ID更新用户的登录状态、积分等);
-
有序键值对统计:需要统计一组唯一key对应的value,并按key有序展示(如统计每个产品的销量,按产品ID有序排列)。
典型示例:用户信息管理(用户ID为唯一key,映射用户姓名、年龄等信息)、字典查询(单词为唯一key,映射释义)、产品销量统计(产品ID为key,映射销量)。
2. multimap 的适用场景(key可重复+键值对+有序)
-
一对多映射存储:需要通过重复key获取多个对应的value,且希望按key有序排列(如班级ID→学生信息、部门ID→员工信息、关键词→相关文章);
-
重复key分组:需要将键值对按key分组,同一个key的多个value放在一起,便于批量获取(如按年级分组展示学生信息、按部门分组展示员工信息);
-
有序多值映射:需要一个有序的键值对队列,允许相同key对应多个value,且能高效查找、删除(如任务优先级→任务列表,相同优先级的任务对应多个)。
典型示例:班级学生管理(班级ID为key,映射该班级所有学生信息)、部门员工管理(部门ID为key,映射该部门所有员工信息)、关键词检索(关键词为key,映射所有相关文章标题)