在 C++ STL(标准模板库)中,map 是最常用的关联式容器 之一,它能将 "键(key)" 与 "值(value)" 一一对应,就像现实中的 "字典"------ 通过 "单词(键)" 找到 "释义(值)"。对于初学者来说,掌握 map 不仅能简化代码逻辑,还能应对大量实际开发场景(如统计频次、映射关系存储等)。本文将结合你提供的代码,从基础概念到实战案例,带你一步步学会 map 的使用。
一、先搞懂:map 是什么?核心特性有哪些?
在学用法之前,先明确 map 的本质和关键特性,这是理解后续操作的基础:
- 键值对存储 :
map中的每个元素都是一个pair<key_type, value_type>(键值对),例如map<string, int>存储 "字符串键 + 整数 value" 的组合。 - 自动排序 :
map会默认按键的升序 自动排序(底层基于红黑树实现),无需手动调用sort,这是它和 "无序关联容器unordered_map" 的核心区别。 - 键唯一 :
map不允许存储重复的键 ------ 如果插入已存在的键,新值会覆盖旧值(或插入失败,取决于插入方式)。 - 高效查找 :由于底层是平衡二叉树,
map的插入、删除、查找操作时间复杂度均为 O(log n),比普通数组遍历(O (n))高效得多。
二、map 基础操作:从你的 map_test 代码学起
你的 map_test 函数包含了 map 的核心基础操作,我们逐行拆解,理解每个步骤的逻辑:
1. 定义 map:指定键和值的类型
cpp
map<string, string> m;
-
格式:
map<键的类型, 值的类型> 容器名。这里key是string类型(比如 "你好""hello"),value也是string类型(比如 "世界""world"),定义了一个 "字符串→字符串" 的映射表m。 -
常见定义示例:
cpp
map<int, string> id_to_name; // 学号(int)→ 姓名(string) map<string, int> word_count; // 单词(string)→ 出现次数(int) map<char, double> char_weight; // 字符(char)→ 权重(double)
2. 插入元素:3 种常用方式
你的代码用了 insert 和 [] 两种插入方式,我们补充完整 3 种常用插入法:
(1)insert({key, value}):初始化列表插入(C++11 及以上)
cpp
m.insert({ "你好", "世界" });
m.insert({ "hello", "world" });
- 最直观的插入方式,直接传入键值对的初始化列表
{key, value}。 - 注意 :如果键已存在,
insert会插入失败 (不会覆盖旧值)。比如你代码中后续插入make_pair("hello", "china1"),由于 "hello" 已存在,这行插入会无效。
(2)insert(make_pair(key, value)):make_pair 构造键值对
cpp
m.insert(make_pair("hello", "china1")); // 插入失败,"hello"已存在
m.insert(make_pair("man", "who")); // 插入成功
make_pair是 STL 提供的工具函数,用于生成一个pair<key_type, value_type>类型的键值对,效果和{key, value}类似。- 同样遵循 "键唯一" 规则:键已存在则插入失败。
(3)map[key] = value:直接赋值插入(最灵活)
cpp
m["hello"] = "china2";
- 这是初学者最常用的插入方式,逻辑是:
- 如果键 "hello" 不存在,就创建一个键值对
("hello", "china2"); - 如果键 "hello" 已存在(之前插入过 "world"),就用新值 "china2"覆盖旧值。
- 如果键 "hello" 不存在,就创建一个键值对
- 你的代码中,这行执行后,"hello" 对应的 value 从 "world" 变成了 "china2"。
3. 遍历 map:2 种常用方式
你的代码用了迭代器遍历,我们补充范围 for 循环(更简洁):
(1)迭代器遍历(灵活,支持中间位置开始)
cpp
map<string, string>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << ":" << it->second << endl;
it++;
}
-
map<string, string>::iterator:定义map的迭代器类型(必须和map的键值类型匹配)。 -
m.begin():指向map的第一个元素(按键升序的第一个);m.end():指向map末尾的 "哨兵位置"(不是最后一个元素)。 -
it->first:获取迭代器指向元素的 "键";it->second:获取 "值"(注意用->而非.,因为it是指针)。 -
遍历结果:由于
map按键升序排序,你的代码输出会是:plaintext
but:the get:it hello:china2 man:who 你好:世界(中文和英文混排时,按 ASCII 码排序,英文在前,中文在后)
(2)范围 for 循环(C++11+,简洁高效)
如果不需要灵活控制遍历位置,范围 for 循环更简单:
cpp
for (auto& e : m) // auto 自动推导 e 为 pair<string, string> 类型
{ // & 引用避免拷贝,提高效率
cout << e.first << ":" << e.second << endl;
}
- 效果和迭代器遍历完全一致,代码更短,适合初学者日常使用。
4. 查找元素:通过键找值
虽然你的 map_test 没写查找,但这是 map 的核心功能,必须掌握:
cpp
// 查找键 "hello"
map<string, string>::iterator find_it = m.find("hello");
if (find_it != m.end()) // 找到:迭代器不等于 m.end()
{
cout << "找到键 'hello',值为:" << find_it->second << endl; // 输出 china2
}
else // 没找到
{
cout << "未找到键 'hello'" << endl;
}
map.find(key):返回指向键为key的元素的迭代器;如果没找到,返回m.end()。- 注意 :不要直接用
m[key]查找(比如if (m["test"] != ""))------ 如果键 "test" 不存在,m["test"]会自动创建一个键值对("test", 空值),导致意外插入。
5. 删除元素:按键或迭代器删除
cpp
// 方式 1:按键删除
m.erase("hello"); // 删除键为 "hello" 的元素
// 方式 2:按迭代器删除(先查找再删除)
auto del_it = m.find("man");
if (del_it != m.end())
{
m.erase(del_it); // 删除迭代器指向的元素
}
// 方式 3:删除所有元素
m.clear(); // 清空 map,size 变为 0
6. 其他常用函数
cpp
cout << "map 中元素个数:" << m.size() << endl; // 输出元素数量
cout << "map 是否为空:" << (m.empty() ? "是" : "否") << endl; // 判断是否为空
三、map 实战案例:从 topKFrequent 看 map 的实际应用
你的 topKFrequent 函数是 map 的经典实战场景 ------"统计单词出现频次,返回前 k 个高频单词",我们分析这个案例,理解 map 在实际问题中的用法:
1. 场景需求
输入一个单词列表 words 和整数 k,返回前 k 个出现次数最多的单词;如果频次相同,按字典序排序。
2. map 的核心作用:统计频次
cpp
map<string, int> m;
for (auto& e : words)
{
m[e]++; // 键是单词 e,值是出现次数,每次遇到 e 就加 1
}
- 这里
map完美解决 "去重 + 统计" 的需求:- 单词第一次出现时,
m[e]初始化为 0,++ 后变为 1; - 单词再次出现时,
m[e]直接加 1,自动累计次数; - 无需手动处理重复单词,
map会通过 "键唯一" 特性自动去重。
- 单词第一次出现时,
3. 为什么需要转存到 vector?
map 按键排序,无法直接按 "值(频次)" 排序,因此需要将 map 的键值对转存到 vector 中,再自定义排序规则:
cpp
vector<pair<string, int>> v1(m.begin(), m.end()); // map 转 vector
vector支持自定义排序,而map不行 ------ 这是map与vector的常见配合用法。
4. 排序与结果提取
cpp
// 自定义排序:先按频次降序,频次相同按字典序升序
sort(v1.begin(), v1.end(), [](const pair<string, int>& a, const pair<string, int>& b) {
return a.second > b.second || (a.second == b.second && a.first < b.first);
});
// 提取前 k 个单词
vector<string> v2;
for (int i = 0; i < k; i++)
{
v2.push_back(v1[i].first);
}
return v2;
- 这里
map完成了 "统计频次" 的核心工作,后续排序和结果提取依赖vector,体现了不同容器的分工配合。
四、初学者常见坑点:避坑指南
- 键的类型必须支持比较 :
map按键排序,因此键的类型(如自定义结构体)必须定义比较规则,否则编译报错(基础类型如int、string已自带比较规则,无需额外处理)。 []操作符的副作用 :用m[key]时,如果键不存在,会自动插入一个 "键为 key、值为默认值" 的元素(比如map<int, int>中,默认值为 0),如果只是想 "查找",不要用[],用find。- 迭代器不支持随机访问 :
map的迭代器是双向迭代器,只能用++或--移动,不能用it + 1或it - 2(和vector的随机访问迭代器不同)。 - 区分
map和unordered_map:如果不需要按键排序,仅需要高效查找(O (1) 平均复杂度),用unordered_map;如果需要按键排序,用map。
五、总结:初学者如何学好 map?
- 先掌握基础操作 :定义、插入(3 种方式)、遍历(2 种方式)、查找(
find)、删除(erase),这是后续应用的基础。 - 结合案例理解 :像
topKFrequent这样的 "统计频次" 场景,是map最常用的场景,多写几遍就能熟练。 - 对比其他容器 :理解
map与vector(顺序存储)、unordered_map(无序关联)的区别,知道什么时候该用哪个容器。