【C++ STL篇(九)】map容器——零基础入门与核心用法精讲

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 系列的初步认识

mapmultimap 都定义在头文件 <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;

这四个模板参数分别代表:

  1. Key :键(关键字)的类型。map 中的所有元素都按键的严格弱序(给C++有序容器用的、不会乱套的合法排序规则,是一种"能分清大小、也能分清谁和谁一样"的比较方式) 排列。
  2. T :映射值的类型。键和值捆绑在一起,形成我们常说的 " 键值对 "。
  3. Compare :键的比较规则,默认是 less<Key>,也就是用 < 运算符进行升序排列。如果你想让键按降序排列,或者键本身不支持 < 比较(比如自定义类型),就可以自己写一个仿函数(函数对象)传进去。
  4. Alloc:内存分配器,一般用默认的就行,它会负责从堆上申请和释放内存。

最核心的一点:map 的底层是由红黑树实现的。 红黑树是一种自平衡的二叉搜索树,它保证了插入、删除、查找操作的时间复杂度都是 O(log N) ,非常高效。同时,二叉搜索树的中序遍历刚好是有序的,所以当我们用迭代器遍历 map 时,拿到的键值对会按键的升序排列。

值得注意的是,map 中的实际存储单元是 value_type,即 pair<const Key, T>。在文档中,map 的成员类型定义如下:

  • key_type -> Key
  • mapped_type -> T
  • value_type -> pair<const Key, T>

小提示:日常交流中,我们习惯把 mapped_type 叫做"value",但要记住它不是 value_typevalue_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,千万别混淆:

  • 第一个 pairinsert 的返回值,它包含一个迭代器和一个布尔值。
  • 第二个 pairmap 存储的元素类型,即键值对。

返回值含义:

  • 如果插入成功 (键不存在),booltrue,迭代器指向新插入的元素
  • 如果插入失败 (键已存在),boolfalse,迭代器指向 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;
}

我们来拆解这个过程:

  1. 调用 insert,传入键 k 和一个默认构造的值(对 int 是 0,对 string 是 空字符串)。
  2. 如果 k 不存在,则插入成功,返回 pair<指向新元素的迭代器, true>
  3. 如果 k 已存在,则插入失败,返回 pair<指向已存在元素的迭代器, false>
  4. 无论哪种情况,取出迭代器指向元素的值并返回引用。

正是利用了 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 不存在时强行插入一个默认值。这种情况下应该使用 findcount

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]++ 的背后发生了什么?

  1. 如果 str 不在 map 中,[] 会插入 {str, 0},返回 0 的引用,然后 ++ 把它变成 1。
  2. 如果 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

multimapmap 几乎是一个模子刻出来的,唯一的本质区别是:multimap 允许存在多个相同的键。 因为键可以重复,它的接口行为也相应地有一些调整。

7.1 插入 insert

map 中,如果键已存在插入会失败;但在 multimap 中,插入永远成功 ,即使键已经存在,也会插入一个新的键值对。因此,multimapinsert 返回值变成了 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

multimapcount返回某个键的实际个数

7.4 删除 erase

如果用键来 erasemultimap删除所有与该键匹配的元素 ,并返回删除的数量。如果只想删除某个特定的键值对,必须使用迭代器版本的 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 随机链表的复制

传送门:随机链表的复制


思路拆解:

  1. 分两次遍历,用map记录新旧节点的对应关系。
  2. 第一次遍历:按顺序复制节点,搭出新链表的基本结构,同时把每个原节点和它对应的新节点存入map中。
  3. 第二次遍历:根据map,转换原节点random指针指向的旧节点,匹配到新链表对应节点,补全所有random指针。
  4. 最后返回:返回新链表的头节点,完成复制。

代码示例:

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个高频单词

题目理解:

  1. 频率高的在前
  2. 频率相同时,按字典顺序升序排列

思路拆解:

这里我们讲解三种方法。

三种方法都先用 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;
    }
};

结语:

今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _


相关推荐
qq_4924484463 小时前
Jmeter Transaction Controller(事务控制器) 的 TPS(每秒事务数)严格固定为 1
java·开发语言·jmeter
数智工坊3 小时前
【SigLIP论文阅读】:重新定义视觉-语言预训练的损失函数——VLA模型的“语言理解“基石
论文阅读·人工智能·算法·计算机视觉·语言模型
2401_858286113 小时前
OS74.【Linux】线程互斥(3) 线程安全、重入
linux·运维·服务器·开发语言·线程
爱喝水的鱼丶3 小时前
SAP-ABAP:数据类型与数据对象 第二篇:底层逻辑篇——数据类型的分类体系与底层存储原理
运维·开发语言·学习·sap·abap
肥胖小羊3 小时前
基于状态机的客户生命周期流转与自动化触达引擎实现
开发语言·python
玄泽幻库3 小时前
【主流版本】JDK安装版下载地址和环境配置方法
java·开发语言·jdk
十年编程老舅3 小时前
Linux NUMA架构深度剖析:内存管理、进程调度与性能优化
linux·数据库·c++·内存管理·numa
西凉的悲伤3 小时前
Java parallelStream并行流
java·开发语言·parallelstream·并行流
少司府3 小时前
C++基础入门:深挖list的那些事
开发语言·数据结构·c++·容器·list·类型转换·类和对象