在 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
(无序关联)的区别,知道什么时候该用哪个容器。