C++ STL篇(九) ------ map 讲解
** 本篇文章将带你从零开始,一步步掌握 map的核心用法 。全程干货,坐稳发车~ ദ്ദി˶ー̀֊ー́ )✧**
文章目录
- [C++ STL篇(九) ------ map 讲解](#C++ STL篇(九) —— map 讲解)
-
- [1. map 系列的初步认识](#1. map 系列的初步认识)
-
- [1.1 map 的模板声明](#1.1 map 的模板声明)
- [1.2 pair:键值对的载体](#1.2 pair:键值对的载体)
- [2. map 的构造](#2. map 的构造)
- [3. map 的迭代器](#3. map 的迭代器)
- [4. map 的增删查操作](#4. map 的增删查操作)
-
- [4.1 插入 insert ------ 注意返回值](#4.1 插入 insert —— 注意返回值)
-
- [4.1.1 insert 的返回值](#4.1.1 insert 的返回值)
- [4.2 查找 find ------ 既能判断存在,又能访问值](#4.2 查找 find —— 既能判断存在,又能访问值)
- [4.3 计数 count ------ 对 map 来说只有 0 或 1](#4.3 计数 count —— 对 map 来说只有 0 或 1)
- [4.4 删除 erase](#4.4 删除 erase)
- [4.5 下界与上界 lower_bound / upper_bound](#4.5 下界与上界 lower_bound / upper_bound)
- [5. map 的数据修改与 operator[]](#5. map 的数据修改与 operator[])
-
- [5.1 通过迭代器修改 value](#5.1 通过迭代器修改 value)
- [5.2 operator[]](#5.2 operator[])
-
- [5.2.1 operator[] 的内部实现原理](#5.2.1 operator[] 的内部实现原理)
- [5.2.2 operator[] 使用示例](#5.2.2 operator[] 使用示例)
- [5.3 统计水果次数:[] 与 insert 的应用对比](#5.3 统计水果次数:[] 与 insert 的应用对比)
-
- [方法一:find + insert](#方法一:find + insert)
- [方法二:operator[] 一行搞定](#方法二:operator[] 一行搞定)
- [6. map 完整使用样例:构造、遍历与修改](#6. map 完整使用样例:构造、遍历与修改)
- [7. multimap:允许键冗余的 map](#7. multimap:允许键冗余的 map)
-
- [7.1 插入 insert](#7.1 插入 insert)
- [7.2 查找 find](#7.2 查找 find)
- [7.3 计数 count](#7.3 计数 count)
- [7.4 删除 erase](#7.4 删除 erase)
- [7.5 不支持 operator[]](#7.5 不支持 operator[])
- [7.6 multimap 完整使用示例](#7.6 multimap 完整使用示例)
- [8. 实战演练](#8. 实战演练)
-
- [8.1 随机链表的复制](#8.1 随机链表的复制)
- [8.2 前K个高频单词](#8.2 前K个高频单词)
-
- [8.2.1 方法一:利用 stable_sort 的稳定性](#8.2.1 方法一:利用 stable_sort 的稳定性)
- [8.2.2 方法二:直接用 sort 排序](#8.2.2 方法二:直接用 sort 排序)
- [8.2.3 方法三:使用优先级队列(大顶堆)](#8.2.3 方法三:使用优先级队列(大顶堆))
- 结语:
1. map 系列的初步认识
map 和 multimap 都定义在头文件 <map> 中
关于 map 的官方参考可以查阅:
1.1 map 的模板声明
先来看 map 的类模板声明:
cpp
template <
class Key, // 键的类型
class T, // 映射值的类型
class Compare = std::less<Key>, // 键的比较方式,默认小于
class Alloc = std::allocator<pair<const Key, T>> // 空间配置器
> class map;
这四个模板参数分别代表:
Key:键(关键字)的类型。map中的所有元素都按键的严格弱序(给C++有序容器用的、不会乱套的合法排序规则,是一种"能分清大小、也能分清谁和谁一样"的比较方式) 排列。T:映射值的类型。键和值捆绑在一起,形成我们常说的 " 键值对 "。Compare:键的比较规则,默认是less<Key>,也就是用<运算符进行升序排列。如果你想让键按降序排列,或者键本身不支持<比较(比如自定义类型),就可以自己写一个仿函数(函数对象)传进去。Alloc:内存分配器,一般用默认的就行,它会负责从堆上申请和释放内存。
最核心的一点:map 的底层是由红黑树实现的。 红黑树是一种自平衡的二叉搜索树,它保证了插入、删除、查找操作的时间复杂度都是 O(log N) ,非常高效。同时,二叉搜索树的中序遍历刚好是有序的,所以当我们用迭代器遍历 map 时,拿到的键值对会按键的升序排列。
值得注意的是,map 中的实际存储单元是 value_type,即 pair<const Key, T>。在文档中,map 的成员类型定义如下:
key_type->Keymapped_type->Tvalue_type->pair<const Key, T>
小提示:日常交流中,我们习惯把
mapped_type叫做"value",但要记住它不是value_type!value_type是整个键值对。
1.2 pair:键值对的载体
在深入学习 map 的操作之前,必须先熟悉 std::pair ,因为插入、访问都离不开它。map 中存储的每一个元素,既不是单纯的键,也不是单纯的值,而是一个"键值对"。标准库用 pair 这个结构体模板来把键和值打包在一起。
pair 的定义简化如下:
cpp
template <class T1, class T2>
struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first; // 键
T2 second; // 值
// 默认构造函数
pair() : first(T1()), second(T2()) {}
// 用键和值直接构造
pair(const T1& a, const T2& b) : first(a), second(b) {}
// 拷贝构造函数(支持不同类型间的转换)
template<class U, class V>
pair(const pair<U, V>& pr) : first(pr.first), second(pr.second) {}
};
可以看到,pair 有两个公开的成员变量:first 代表键,second 代表值。在 map 里,first 的类型是 const Key,这意味着键一旦存入 map 就不能被修改 ,因为修改键会破坏红黑树的排序结构;而值 second 是可以修改的。
为了方便创建 pair 对象,标准库还提供了一个工具函数 make_pair:
cpp
template <class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y) {
return pair<T1, T2>(x, y);
}
有了它,我们就不用写冗长的类型声明,编译器会自动推导类型。例如 make_pair("hello", 5) 会生成一个 pair<const char*, int> 对象。
在 C++11 之后,你还可以直接用花括号初始化列表来构造 pair,编译器会自动转换:
cpp
map<string, int> m;
m.insert({"apple", 3}); // {"apple", 3} 被隐式转换为 pair<const string, int>
2. map 的构造
map 提供了多种构造方法,让我们灵活地初始化容器。常用的有以下几个:
cpp
// 1. 默认构造:创建一个空的 map
explicit map(const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
// 2. 迭代器区间构造:用 [first, last) 范围内的元素初始化
template <class InputIterator>
map(InputIterator first, InputIterator last,
const key_compare& comp = key_compare(),
const allocator_type& = allocator_type());
// 3. 拷贝构造:用另一个 map 创建一份完全相同的副本
map(const map& x);
// 4. 列表构造(C++11):用初始化列表直接填充 map
map(initializer_list<value_type> il,
const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
来看实际用法:
cpp
map<string, string> dict1; // 默认构造,空 map
map<string, string> dict2 = {
{"left", "左边"},
{"right", "右边"},
{"insert", "插入"},
{"string", "字符串"}
}; // 列表构造,直接初始化
map<string, string> dict3(dict2); // 拷贝构造
map<string, string> dict4(dict2.begin(), dict2.end()); // 迭代器区间构造
最常用的是列表初始化和默认构造后逐个插入元素。
3. map 的迭代器
map 的迭代器是双向迭代器 ,意味着你可以 ++ 向前移动,也可以 -- 向后移动。由于底层是红黑树,迭代器遍历走的是中序遍历,所以遍历顺序就是键的升序。
cpp
iterator begin(); // 指向第一个元素(即键最小的元素)
iterator end(); // 指向最后一个元素的下一个位置
reverse_iterator rbegin(); // 指向最后一个元素
reverse_iterator rend(); // 指向第一个元素的前一个位置
一个非常重要的规则是:通过迭代器可以修改元素的值(second),但不能修改键(first)。 因为 first 的类型是 const Key,任何修改它的企图都会编译报错。这是为了保护底层红黑树的有序结构不被破坏。
此外,有了迭代器,map 就天然支持范围 for 循环:
cpp
map<string, int> m = {{"a",1}, {"b",2}};
for (const auto& e : m)
{
cout << e.first << " : " << e.second << endl;
}
4. map 的增删查操作
map 的增删查接口和 set 极为相似,区别只在于操作的对象变成了键值对,而且一些接口的参数只接受 key(因为 key 就是搜索的依据)。
4.1 插入 insert ------ 注意返回值
map 提供了三种 insert 重载:
cpp
// 1. 插入单个元素,返回值是 pair<iterator, bool>
pair<iterator, bool> insert(const value_type& val);
// 2. 插入初始化列表(C++11)
void insert(initializer_list<value_type> il);
// 3. 插入一个迭代器区间内的所有元素
template <class InputIterator>
void insert(InputIterator first, InputIterator last);
最常用的是第一种------插入单个键值对。先看示例:
cpp
map<string, string> dict;
// 方式1:使用匿名 pair 对象
dict.insert(pair<string, string>("second", "第二个"));
// 方式2:使用 make_pair(类型自动推导)
dict.insert(make_pair("sort", "排序"));
// 方式3:C++11 统一初始化列表
dict.insert({"auto", "自动的"});
// 方式4:先创建 pair 再插入
pair<string, string> kv("first", "第一个");
dict.insert(kv);
这里要多说一句,insert 只关心键是否已存在。如果键已经存在于 map 中,即使你提供的值不同,插入也会失败,原来键对应的值保持原样。 例如:
cpp
dict.insert({"auto", "自动的"});
// 第二次插入同样的键 "auto",值不同,但不会覆盖
dict.insert({"auto", "自动的xxx"});
// 最终 "auto" 对应的值仍然是 "自动的"
4.1.1 insert 的返回值
单个元素插入的返回值类型是 pair<iterator, bool>,这里有两个 pair,千万别混淆:
- 第一个
pair是insert的返回值,它包含一个迭代器和一个布尔值。- 第二个
pair是map存储的元素类型,即键值对。
返回值含义:
- 如果插入成功 (键不存在),
bool为true,迭代器指向新插入的元素。- 如果插入失败 (键已存在),
bool为false,迭代器指向map中已存在的那个相同键的元素。
这相当于说:无论插入成功与否,返回值中的迭代器都指向了该键所在的那个元素。 这个特性正是 operator[] 能够实现的基础,后面会详细展开。
4.2 查找 find ------ 既能判断存在,又能访问值
cpp
// 查找键 k,返回指向该键值对的迭代器,未找到返回 end()
iterator find(const key_type& k);
const_iterator find(const key_type& k) const;
find 接收一个键 k,如果找到,返回指向该键值对的迭代器;如果没找到,返回 end()。通过返回的迭代器,我们不仅可以判断键是否存在,还能直接修改对应的值。
cpp
map<string, int> countMap;
auto it = countMap.find("apple");
if (it != countMap.end())
{
// 找到了,可以读取或修改值
it->second++;
} else {
// 没找到
}
与 set 一样,永远优先使用 map 自身的 find ,它的复杂度是 O(log N),而全局 std::find 会退化成 O(N)。
4.3 计数 count ------ 对 map 来说只有 0 或 1
cpp
size_type count(const key_type& k) const;
由于 map 的键是唯一的,count 的返回值只能是 0(不存在)或 1(存在)。它通常用于快速判断键是否存在,但不能获取对应的值。这个接口在 multimap 中会更有用武之地。
4.4 删除 erase
cpp
// 1. 删除迭代器指向的元素,返回被删除元素的下一个元素的迭代器(C++11起)
iterator erase(const_iterator position);
// 2. 删除键为 k 的元素,返回删除的元素个数(map 中为 0 或 1)
size_type erase(const key_type& k);
// 3. 删除一个迭代器区间 [first, last) 内的所有元素
iterator erase(const_iterator first, const_iterator last);
示例:
cpp
map<string, int> m = {{"a",1}, {"b",2}, {"c",3}};
m.erase("b"); // 删除键"b",返回 1
auto it = m.find("c");
if (it != m.end())
m.erase(it); // 删除迭代器指向的元素
删除操作的语义和 set 完全一致,删除后相应的迭代器会失效,不能继续使用。
4.5 下界与上界 lower_bound / upper_bound
cpp
iterator lower_bound(const key_type& k); // 返回第一个键 >= k 的元素
iterator upper_bound(const key_type& k); // 返回第一个键 > k 的元素
这两个接口主要用于区间查找,在需要批量删除或遍历某个键值范围内的元素时特别有用。
5. map 的数据修改与 operator[]
前面提到过,通过迭代器可以修改 it->second,这是 map 修改 value 的最直接方式。但 map 还有一个极具特色的接口------operator[],它让修改和访问变得异常简洁。
5.1 通过迭代器修改 value
只要拿到指向某个元素的非 const 迭代器,我们就可以修改它的 second,即映射值。
cpp
auto it = dict.find("left");
if (it != dict.end())
{
it->second = "左边(新)"; // 修改成功
}
// it->first = "right"; // 错误!first 是 const,不能修改
5.2 operator[]
operator[] 是 map 中最强大也最易迷惑的接口。它的声明是:
cpp
mapped_type& operator[](const key_type& k);
它具备三种功能:查找、插入、修改 ,具体行为取决于键 k 是否已经存在:
- 如果
k已经存在 :operator[]直接返回该键对应的值的引用。我们可以读取它,也可以修改它。这时的行为相当于查找 + 修改。- 如果
k不存在 :operator[]会插入一个新的键值对,键为k,值为mapped_type的默认值(比如int默认是 0,string默认是空字符串),然后返回这个新插入的值的引用。这时的行为相当于插入 + 修改。
也就是说,operator[] 无论何时都能返回一个合法的值的引用,我们可以直接通过它访问或赋值。
5.2.1 operator[] 的内部实现原理
标准库中 operator[] 的典型实现等价于以下代码:
cpp
mapped_type& operator[](const key_type& k) {
// 1. 尝试插入键值对 {k, mapped_type()} ,mapped_type() 是值的默认构造对象
pair<iterator, bool> ret = insert({ k, mapped_type() });
// 2. 无论插入成功还是失败,ret.first 都指向键 k 所在的元素
iterator it = ret.first;
// 3. 返回该元素中值的引用
return it->second;
}
我们来拆解这个过程:
- 调用
insert,传入键k和一个默认构造的值(对int是 0,对string是 空字符串)。- 如果
k不存在,则插入成功,返回pair<指向新元素的迭代器, true>。- 如果
k已存在,则插入失败,返回pair<指向已存在元素的迭代器, false>。- 无论哪种情况,取出迭代器指向元素的值并返回引用。
正是利用了 insert 返回值中迭代器始终指向键所在元素 的特性,operator[] 巧妙地融合了查找与插入。有了这个接口,很多操作变得异常简洁。
5.2.2 operator[] 使用示例
cpp
map<string, string> dict;
// 场景1:"insert" 不存在,所以会插入 {"insert", ""},然后返回空字符串的引用
dict["insert"];
// 场景2:"left" 不存在,插入 {"left", ""},然后立即赋值为 "左边"
dict["left"] = "左边";
// 场景3:"left" 已存在,直接返回其值的引用,然后修改为 "左边,剩余"
dict["left"] = "左边,剩余";
// 场景4:读取 "left" 的值,这里必须确保 "left" 存在
cout << dict["left"] << endl; // 输出 "左边,剩余"
// 场景5:读取 "right",但 "right" 不存在,会插入 {"right", ""},并输出空字符串
cout << dict["right"] << endl;
必须警惕的是 :如果你只是想单纯检查一个 key 是否存在而不想插入新元素,绝对不要使用
operator[],因为它会在 key 不存在时强行插入一个默认值。这种情况下应该使用find或count。
5.3 统计水果次数:[] 与 insert 的应用对比
这是一个非常经典的 map 应用------统计数组中每个单词出现的次数。我们可以用 find + insert 的传统写法,也可以用 operator[] 写出极其优雅的代码。
方法一:find + insert
cpp
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (const auto& str : arr)
{
// 先查找这个水果是否已经在 map 中
auto ret = countMap.find(str);
if (ret == countMap.end())
{
// 第一次出现,插入键值对 {水果, 1}
countMap.insert({str, 1});
}
else
{
// 已经存在,将对应次数加 1
ret->second++;
}
}
for (const auto& e : countMap)
{
cout << e.first << ":" << e.second << endl;
}
方法二:operator[] 一行搞定
cpp
map<string, int> countMap;
for (const auto& str : arr)
{
countMap[str]++; // 简洁到不可思议!
}
countMap[str]++ 的背后发生了什么?
- 如果
str不在map中,[]会插入{str, 0},返回 0 的引用,然后++把它变成 1。 - 如果
str已存在,[]直接返回当前次数的引用,然后++让它加 1。
6. map 完整使用样例:构造、遍历与修改
下面综合演示 map 的构造、插入、遍历、以及 key 不可改但 value 可改的特性。
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
// 列表构造
map<string, string> dict =
{
{"left", "左边"},
{"right", "右边"},
{"insert", "插入"},
{"string", "字符串"}
};
// 多种插入方式
dict.insert(pair<string, string>("second", "第二个"));
dict.insert(make_pair("sort", "排序"));
dict.insert({"auto", "自动的"});
// 测试插入重复 key(不会覆盖原有 value)
dict.insert({"auto", "自动的xxx"}); // 插入失败,auto 的值仍为 "自动的"
// 遍历 map,使用迭代器
map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
// 尝试修改 key 是不允许的,下面这行编译会报错
// it->first += 'x';
// 但可以修改 value,我们在每个 value 后面加上 'x' 作为标记
it->second += 'x';
// 输出键值对。it->first 是键,it->second 是值
// 这里演示几种访问方式,结果相同
cout << it->first << ":" << it->second << endl;
// cout << (*it).first << ":" << (*it).second << endl; // 等价写法
// cout << it.operator->()->first << ":" << it.operator->()->second << endl; // 底层写法
++it;
}
cout << endl;
return 0;
}
输出结果按照键的字典序排列,每个 value 后面多了一个 x:

要点解析:
- 构造时可以直接用花括号列表,每个元素也是一个花括号对,非常直观。
- 插入时,
dict.insert({"auto", "自动的"});是最简洁的写法。make_pair也依然常用。 - 注意第二次
insert({"auto", "自动的xxx"}),由于 key"auto"已经存在,插入失败,原值"自动的"保持不变。 - 遍历时,
it->first获取 key,it->second获取 value。尝试修改it->first会触发编译错误,而修改it->second完全合法。
7. multimap:允许键冗余的 map
multimap 和 map 几乎是一个模子刻出来的,唯一的本质区别是:multimap 允许存在多个相同的键。 因为键可以重复,它的接口行为也相应地有一些调整。
7.1 插入 insert
在 map 中,如果键已存在插入会失败;但在 multimap 中,插入永远成功 ,即使键已经存在,也会插入一个新的键值对。因此,multimap 的 insert 返回值变成了 iterator(指向新插入的元素),不再需要 bool 来指示成败。
7.2 查找 find
multimap 中可能有多个相同的键,find 会返回中序遍历的第一个匹配元素的迭代器。要遍历所有相同键的元素,可以这样:
cpp
auto it = dict.find("sort");
while (it != dict.end() && it->first == "sort")
{
cout << it->second << endl;
++it;
}
7.3 计数 count
在 multimap 中 count返回某个键的实际个数。
7.4 删除 erase
如果用键来 erase,multimap 会删除所有与该键匹配的元素 ,并返回删除的数量。如果只想删除某个特定的键值对,必须使用迭代器版本的 erase。
7.5 不支持 operator[]
这一点很好理解:如果允许键冗余,operator[] 就无法确定该返回哪个键对应的值。因此 multimap 没有 operator[],想要修改值只能通过迭代器。
7.6 multimap 完整使用示例
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
multimap<string, string> dict;
// 插入永远成功,即使键相同
dict.insert({"sort", "排序"});
dict.insert({"sort", "排序1"});
dict.insert({"sort", "排序2"});
dict.insert({"sort", "排序3"});
dict.insert({"sort", "排序"}); // 重复的键值对也允许
dict.insert({"string", "字符串"});
// 遍历,会看到所有 "sort" 键的元素按中序排列
for (const auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
// count 统计键的个数
cout << "sort count: " << dict.count("sort") << endl; // 输出 5
// erase 按键删除,会删除所有匹配的
dict.erase("sort"); // 所有 "sort" 元素都被删除
cout << "sort count after erase: " << dict.count("sort") << endl; // 输出 0
return 0;
}

关键差异总结:
| 特性 | map | multimap |
|---|---|---|
| 键唯一性 | 键唯一 | 允许重复键 |
operator[] |
支持 | 不支持 |
insert 返回值 |
pair<iterator, bool> |
仅 iterator(总成功) |
find |
返回唯一元素的迭代器或 end() | 返回中序第一个 |
count |
0 或 1 | 实际个数 |
erase(key) |
删除该键的元素(0 或 1 个) | 删除所有该键的元素 |
8. 实战演练
8.1 随机链表的复制
传送门:随机链表的复制

思路拆解:
- 分两次遍历,用map记录新旧节点的对应关系。
- 第一次遍历:按顺序复制节点,搭出新链表的基本结构,同时把每个原节点和它对应的新节点存入map中。
- 第二次遍历:根据map,转换原节点random指针指向的旧节点,匹配到新链表对应节点,补全所有random指针。
- 最后返回:返回新链表的头节点,完成复制。
代码示例:
cpp
class Solution {
public:
Node* copyRandomList(Node* head) {
map<Node*,Node*> Map;
Node* copyhead = nullptr,*copytail = nullptr;
Node* cur = head;
while(cur)
{
if(copytail == nullptr)
{
copyhead = copytail = new Node(cur->val);
}
else
{
copytail->next = new Node(cur->val);
copytail = copytail->next;
}
Map[cur] = copytail;
cur = cur->next;
}
cur = head;
Node* copy = copyhead;
while(cur)
{
if(cur->random == nullptr)
{
copy->random = nullptr;
}
else
{
copy->random = Map[cur->random];
}
cur = cur->next;
copy = copy->next;
}
return copyhead;
}
};
8.2 前K个高频单词
传送门:前K个高频单词

题目理解:
- 频率高的在前
- 频率相同时,按字典顺序升序排列
思路拆解:
这里我们讲解三种方法。
三种方法都先用 map<string, int> 统计每个单词的出现次数。
cpp
map<string,int> countMap;
for(auto& e : words)
{
countMap[e]++;
}
选择 map 的好处:它会按 字典序 自动排序。
8.2.1 方法一:利用 stable_sort 的稳定性
整体思路:
- countMap 已经是 字典序升序。
- 只需要再按频率降序排序,频率相同的元素,保持它们原来在 map 中的字典序。
- stable_sort 是稳定排序:相等的元素排序前后相对顺序不变。
比较器设计:
cpp
struct Compare
{
bool operator()(const pair<string,int>& kv1, const pair<string,int>& kv2)
{
return kv1.second > kv2.second;
}
};
- 比较器只管频率:
kv1.second > kv2.second为真时,kv1 排在 kv2 前面(频率降序)。 - 频率相等时返回 false,stable_sort 不动它们的顺序 -> 保留 map 原有的字典序。
代码:
cpp
class Solution {
public:
//实现一个仿函数
struct Compare
{
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2)
{
return kv1.second > kv2.second;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
for(auto& e : words)
{
countMap[e]++;//把单词和它出现的次数绑定起来
}
vector<pair<string,int>> v(countMap.begin(),countMap.end());
stable_sort(v.begin(),v.end(),Compare());//放入vector中排序
vector<string> str;
for(int i = 0;i < k;i++)
{
str.push_back(v[i].first);//取前 K个单词
}
return str;
}
};
8.2.2 方法二:直接用 sort 排序
整体思路:
sort 是非稳定排序,不能依赖原有顺序,必须在比较器中同时定义频率降序和字典序升序。
比较器设计:
cpp
struct Compare
{
bool operator()(const pair<string,int>& kv1, const pair<string,int>& kv2)
{
return kv1.second > kv2.second ||
(kv1.second == kv2.second && kv1.first < kv2.first);
}
};
先看频率:kv1.second > kv2.second -> 频率更高的排前面。
频率相等时:kv1.first < kv2.first -> 字典序更小的排前面。
代码:
cpp
class Solution {
public:
struct Compare
{
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2)
{
return kv1.second > kv2.second ||
(kv1.second == kv2.second && kv1.first < kv2.first);
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
for(auto& e : words)
{
countMap[e]++;
}
vector<pair<string,int>> v(countMap.begin(),countMap.end());
sort(v.begin(),v.end(),Compare());
vector<string> str;
for(int i = 0;i < k;i++)
{
str.push_back(v[i].first);
}
return str;
}
};
8.2.3 方法三:使用优先级队列(大顶堆)
整体思路:
不需要全排序,构建一个大顶堆,堆顶始终是"最符合要求"的元素,连续弹出 k 次即可。
比较器设计(关键)
cpp
struct Compare
{
bool operator()(const pair<string,int>& kv1, const pair<string,int>& kv2)
{
return kv1.second < kv2.second ||
(kv1.second == kv2.second && kv1.first > kv2.first);
}
};
前提:priority_queue 的第三个模板参数是"比较类",当 Compare()(a, b) 返回 true 时,说明 a 的优先级低于 b ,堆会把优先级高的放堆顶。
kv1.second < kv2.second-> kv1 频率更低 -> kv1 优先级低于 kv2 -> kv2 更容易靠近堆顶(频率高的在堆顶)。- 频率相等时:
kv1.first > kv2.first-> kv1 字典序更大 -> kv1 优先级低于 kv2 -> kv2 更容易在堆顶(字典序小的在堆顶)。
代码:
cpp
class Solution {
public:
struct Compare
{
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2)
{
return kv1.second < kv2.second ||
(kv1.second == kv2.second && kv1.first > kv2.first);
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
for(auto& e : words)
{
countMap[e]++;
}
priority_queue<pair<string,int>,vector<pair<string,int>>,Compare> pq(countMap.begin(),countMap.end());
vector<string> str;
for(int i = 0;i < k;i++)
{
str.push_back(pq.top().first);
pq.pop();
}
return str;
}
};
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _