C++进阶:(五)map系列容器的全面解析

目录

前言

[一、map 系列底层基石:红黑树与键值对存储](#一、map 系列底层基石:红黑树与键值对存储)

[1.1 红黑树:保证高效增删查改的核心](#1.1 红黑树:保证高效增删查改的核心)

[1.2 键值对(pair):map 的核心数据单元](#1.2 键值对(pair):map 的核心数据单元)

[1.2.1 pair 结构体的本质](#1.2.1 pair 结构体的本质)

[1.2.2 map 与 pair 的关联](#1.2.2 map 与 pair 的关联)

[二、map 容器核心特性与模板参数](#二、map 容器核心特性与模板参数)

[2.1 map 的模板参数解析](#2.1 map 的模板参数解析)

[2.2 map 的核心特性](#2.2 map 的核心特性)

[三、map 容器核心接口详解与实战示例](#三、map 容器核心接口详解与实战示例)

[3.1 构造函数与初始化](#3.1 构造函数与初始化)

[3.1.1 构造接口](#3.1.1 构造接口)

[3.1.2 示例:多种初始化方式](#3.1.2 示例:多种初始化方式)

[3.2 迭代器与遍历](#3.2 迭代器与遍历)

[3.2.1 迭代器接口](#3.2.1 迭代器接口)

[3.2.2 迭代器访问规则](#3.2.2 迭代器访问规则)

[3.2.3 示例:多种遍历方式](#3.2.3 示例:多种遍历方式)

[3.3 插入操作(insert)](#3.3 插入操作(insert))

[3.3.1 插入接口](#3.3.1 插入接口)

[3.3.2 关键接口解析(单个键值对插入)](#3.3.2 关键接口解析(单个键值对插入))

[3.3.3 插入键值对的 4 种方式](#3.3.3 插入键值对的 4 种方式)

[3.3.4 示例:插入操作](#3.3.4 示例:插入操作)

[3.4 查找操作(find/count/lower_bound/upper_bound)](#3.4 查找操作(find/count/lower_bound/upper_bound))

[3.4.1 查找接口](#3.4.1 查找接口)

[3.4.2 接口使用场景](#3.4.2 接口使用场景)

[3.4.3 示例:查找操作](#3.4.3 示例:查找操作)

[3.5 删除操作(erase)](#3.5 删除操作(erase))

[3.5.1 删除接口](#3.5.1 删除接口)

[3.5.2 关键注意事项](#3.5.2 关键注意事项)

[3.5.3 示例:删除操作](#3.5.3 示例:删除操作)

[3.6 数据修改:迭代器与 operator [] 的核心用法](#3.6 数据修改:迭代器与 operator [] 的核心用法)

[3.6.1 迭代器修改 value](#3.6.1 迭代器修改 value)

[3.6.2 operator []:多功能复合接口](#3.6.2 operator []:多功能复合接口)

[1. operator [] 的声明与实现原理](#1. operator [] 的声明与实现原理)

[2. operator [] 的三重功能](#2. operator [] 的三重功能)

[3. 实战示例:operator [] 的用法](#3. 实战示例:operator [] 的用法)

[4. operator [] 的注意事项](#4. operator [] 的注意事项)

[四、multimap 容器:支持 key 冗余的键值对映射](#四、multimap 容器:支持 key 冗余的键值对映射)

[4.1 multimap 的核心特性](#4.1 multimap 的核心特性)

[4.2 multimap 与 map 的接口差异](#4.2 multimap 与 map 的接口差异)

[4.3 示例:multimap 的使用](#4.3 示例:multimap 的使用)

[4.4 multimap 的适用场景](#4.4 multimap 的适用场景)

[五、map 系列容器的OJ题实战](#五、map 系列容器的OJ题实战)

[5.1 场景一:随机链表的复制(键值对映射解决指针关联)](#5.1 场景一:随机链表的复制(键值对映射解决指针关联))

[解题思路(基于 map)](#解题思路(基于 map))

代码实现

复杂度分析

[5.2 场景二:前 K 个高频单词(map 统计 + 排序)](#5.2 场景二:前 K 个高频单词(map 统计 + 排序))

代码实现

复杂度分析

总结


前言

在 C++ STL 容器体系中,map 系列是关联式容器的核心成员之一,专为键值对(key-value) 存储与高效查找场景设计。与 set 系列专注于 "单一关键字" 不同,map 系列通过 "关键字 - 值" 的映射关系,实现了数据的结构化存储与快速访问,广泛应用于字典查询、数据统计、缓存映射等场景。下面就让我们正式开始吧!


一、map 系列底层基石:红黑树与键值对存储

首先给大家提供一下map系列的参考文档:https://legacy.cplusplus.com/reference/map/

与set系列容器类似,map 系列(map、multimap)的高效性源于底层的红黑树(平衡二叉搜索树) 实现,而其核心功能则依赖于 "键值对" 的结构化存储。理解这两个基础要素,是掌握 map 系列的关键。下面我们就先来复习一下红黑树的相关知识:

1.1 红黑树:保证高效增删查改的核心

map 系列与 set 系列共享相同的底层数据结构 ------ 红黑树。红黑树通过以下 5 条规则确保自身始终处于 "近似平衡" 状态,从而使增删查改操作的时间复杂度稳定在O(log N)

  1. 每个节点要么是红色,要么是黑色;
  2. 根节点必须是黑色;
  3. 所有叶子节点(NIL 节点,空节点)均为黑色;
  4. 若一个节点是红色,则其两个子节点必须是黑色(避免连续红节点);
  5. 从任意节点到其所有后代叶子节点的路径上,黑色节点的数量相同(确保路径长度差异不超过 2 倍)。

这种平衡特性使得 map 系列在处理大规模数据时,依然能保持高效的操作性能,避免了普通二叉搜索树在极端情况下(如有序插入)退化为链表、导致 O (N) 时间复杂度的问题。

1.2 键值对(pair):map 的核心数据单元

map 系列存储的数据并非单一值,而是**"关键字(key)- 值(value)"** 的映射对,底层通过**pair<const Key, T>**结构体实现这一映射关系。pair结构体的定义与特性如下所示:

1.2.1 pair 结构体的本质

pair是 STL 中用于存储两个关联数据的模板结构体,其简化版的源码定义如下:

cpp 复制代码
template <class T1, class T2>
struct pair {
    typedef T1 first_type;  // 第一个成员的类型(对应map的key)
    typedef T2 second_type; // 第二个成员的类型(对应map的value)

    T1 first;  // 关键字(key),在map中为const类型,不可修改
    T2 second; // 值(value),可修改

    // 无参构造:默认初始化两个成员
    pair() : first(T1()), second(T2()) {}

    // 带参构造:通过指定值初始化
    pair(const T1& a, const T2& b) : first(a), second(b) {}

    // 拷贝构造:从其他pair对象拷贝初始化
    template<class U, class V>
    pair(const pair<U, V>& pr) : first(pr.first), second(pr.second) {}
};

1.2.2 map 与 pair 的关联

在 map 中,pairfirst成员被限定为const 类型const Key),这是因为 key 是红黑树排序的依据 ------ 修改 key 会破坏红黑树的有序结构,导致容器功能异常;而second成员(value)则可以自由修改,满足 "通过 key 查找并更新 value" 的需求。

为了方便创建pair对象,STL 提供了make_pair函数,无需显式指定模板参数即可生成pair实例:

cpp 复制代码
template <class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y) {
    return pair<T1, T2>(x, y);
}

例如,创建一个 **"字符串 - 整数"**的键值对,可通过以下两种方式:

cpp 复制代码
// 1. 直接构造pair(需显式指定模板参数)
pair<string, int> kv1("apple", 5);

// 2. 通过make_pair构造(自动推导类型,更简洁)
auto kv2 = make_pair("banana", 3);

二、map 容器核心特性与模板参数

map 是 map 系列中最常用的容器,其关键是 "有序、唯一键值对映射"------ 即 key 唯一且按指定规则排序,value 可通过 key 快速查找与修改。

2.1 map 的模板参数解析

map 的模板定义决定了其 key 类型、value 类型、排序规则与内存分配方式,其完整声明如下:

cpp 复制代码
template < 
    class Key,                     // 1. 关键字(key)的类型
    class T,                       // 2. 值(value)的类型
    class Compare = less<Key>,     // 3. 排序规则(默认升序)
    class Alloc = allocator<pair<const Key, T>>  // 4. 空间配置器
> class map;

各模板参数的作用:

  1. Key :即键的类型,需支持Compare指定的比较规则(默认支持<运算符)。例如**map<int, string>**中,Key 为int类型。

  2. T :即值的类型,可任意指定(如intstring、自定义结构体等)。在 map 中,T 被 typedef 为mapped_type,而value_type则是pair<const Key, T>(红黑树节点存储的实际类型)。

  3. Compare :用于定义 key 的排序规则,默认使用less<Key>(按 key 升序排列)。若需自定义排序(如降序、按字符串长度排序),可以传入自定义仿函数或 STL 内置仿函数。

  4. Alloc :空间配置器,负责内存的分配与释放,默认使用 STL 提供的allocator。一般情况下无需手动指定,仅在特殊内存管理场景(如内存池)中需要自定义。

示例:创建一个按 key 降序排列的 map:

cpp 复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    // 自定义排序规则:按key降序(使用STL内置的greater<int>)
    map<int, string, greater<int>> descMap = {
        {3, "apple"}, 
        {1, "banana"}, 
        {2, "orange"}
    };

    // 遍历:按key降序输出
    for (const auto& kv : descMap) {
        cout << kv.first << ": " << kv.second << endl;
    }
    /* 输出结果:
    3: apple
    2: orange
    1: banana
    */
    return 0;
}

2.2 map 的核心特性

基于底层红黑树与键值对存储,map 具备以下核心特性:

  1. key 唯一:map 中不允许存在重复的 key,插入已存在的 key 会失败;
  2. key 有序 :元素按Compare指定的规则排序,遍历顺序为红黑树的中序遍历结果;
  3. 高效操作 :增删查改的时间复杂度均为O (log N),源于红黑树的平衡特性;
  4. 迭代器特性
    • 支持双向迭代器(iteratorreverse_iterator),可正向 / 反向遍历;
    • iterator指向的pair中,**first(key)**为 const 类型,不可修改;**second(value)**可修改;
    • const_iterator则完全只读,既不能修改 key,也不能修改 value;
  5. 支持范围 for:C++11 及以上标准中,可通过范围 for 简化遍历操作。

三、map 容器核心接口详解与实战示例

STL 容器的接口设计具有高度一致性,map 的接口与 set 系列有诸多相似之处,但也因 "键值对" 特性存在独特接口(如operator[])。本节详解 map 的各个核心接口(构造、迭代器、增删查改),并通过实战示例说明使用场景。

3.1 构造函数与初始化

map 主要提供了 4 种构造方式,覆盖空构造、区间构造、拷贝构造与初始化列表构造,满足不同场景的初始化需求。

3.1.1 构造接口

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& alloc = allocator_type());

// 3. 拷贝构造:用另一个map对象初始化
map(const map& x);

// 4. 初始化列表构造:用初始化列表中的键值对初始化(最常用)
map(initializer_list<value_type> il,
    const key_compare& comp = key_compare(),
    const allocator_type& alloc = allocator_type());

3.1.2 示例:多种初始化方式

cpp 复制代码
#include <iostream>
#include <map>
#include <vector>
using namespace std;

int main() {
    // 1. 无参构造
    map<int, string> m1;
    m1.insert(make_pair(1, "one"));
    m1.insert({2, "two"});  // C++11列表初始化简化
    cout << "m1(无参构造+插入):" << endl;
    for (const auto& kv : m1) {
        cout << kv.first << ": " << kv.second << " ";
    }
    cout << endl;  // 输出:1: one 2: two 

    // 2. 初始化列表构造(最简洁,推荐)
    map<int, string> m2 = {
        {3, "three"}, 
        {4, "four"}, 
        {5, "five"}
    };
    cout << "m2(初始化列表构造):" << endl;
    for (const auto& kv : m2) {
        cout << kv.first << ": " << kv.second << " ";
    }
    cout << endl;  // 输出:3: three 4: four 5: five 

    // 3. 迭代器区间构造(用m2的部分区间初始化m3)
    auto start = ++m2.begin();  // 指向4: four
    auto end = m2.end();        // 指向尾后迭代器
    map<int, string> m3(start, end);
    cout << "m3(迭代器区间构造):" << endl;
    for (const auto& kv : m3) {
        cout << kv.first << ": " << kv.second << " ";
    }
    cout << endl;  // 输出:4: four 5: five 

    // 4. 拷贝构造(用m2初始化m4)
    map<int, string> m4(m2);
    cout << "m4(拷贝构造):" << endl;
    for (const auto& kv : m4) {
        cout << kv.first << ": " << kv.second << " ";
    }
    cout << endl;  // 输出:3: three 4: four 5: five 

    return 0;
}

3.2 迭代器与遍历

map 支持双向迭代器,遍历方式包括 "迭代器遍历" 与 "范围 for 遍历",遍历顺序由Compare规则决定(默认升序)。

3.2.1 迭代器接口

cpp 复制代码
// 正向迭代器:指向第一个元素
iterator begin();
const_iterator begin() const;

// 正向尾后迭代器:不指向任何元素,作为遍历结束标志
iterator end();
const_iterator end() const;

// 反向迭代器:指向最后一个元素(逆序遍历的起点)
reverse_iterator rbegin();
const_reverse_iterator rbegin() const;

// 反向尾后迭代器:作为逆序遍历的结束标志
reverse_iterator rend();
const_reverse_iterator rend() const;

3.2.2 迭代器访问规则

  • 迭代器指向的是pair<const Key, T>对象,需通过->first访问 key->second访问 value
  • 非 const 迭代器可修改second(value),但不可修改first(key);
  • const 迭代器完全只读,不可修改任何成员。

3.2.3 示例:多种遍历方式

cpp 复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    map<string, int> fruitCount = {
        {"apple", 5}, 
        {"banana", 3}, 
        {"orange", 4}
    };

    // 1. 正向非const迭代器遍历(可修改value)
    cout << "正向遍历(修改value前):" << endl;
    auto it = fruitCount.begin();
    while (it != fruitCount.end()) {
        // it->first = "pear";  // 错误:key为const,不可修改
        cout << it->first << ": " << it->second << " ";
        it->second += 1;  // 合法:修改value
        ++it;
    }
    cout << endl;  // 输出:apple: 5 banana: 3 orange: 4 

    // 2. 正向const迭代器遍历(完全只读)
    cout << "正向const遍历(修改value后):" << endl;
    map<string, int>::const_iterator cit = fruitCount.cbegin();
    while (cit != fruitCount.cend()) {
        // cit->second += 1;  // 错误:const迭代器不可修改value
        cout << cit->first << ": " << cit->second << " ";
        ++cit;
    }
    cout << endl;  // 输出:apple: 6 banana: 4 orange: 5 

    // 3. 反向迭代器遍历(逆序输出)
    cout << "反向遍历:" << endl;
    map<string, int>::reverse_iterator rit = fruitCount.rbegin();
    while (rit != fruitCount.rend()) {
        cout << rit->first << ": " << rit->second << " ";
        ++rit;
    }
    cout << endl;  // 输出:orange: 5 banana: 4 apple: 6 

    // 4. 范围for遍历(最简洁,推荐)
    cout << "范围for遍历:" << endl;
    for (const auto& kv : fruitCount) {
        cout << kv.first << ": " << kv.second << " ";
    }
    cout << endl;  // 输出:apple: 6 banana: 4 orange: 5 

    return 0;
}

3.3 插入操作(insert)

插入是 map 的核心接口之一,负责向红黑树中添加键值对,同时维持 key 的唯一性与有序性。

3.3.1 插入接口

cpp 复制代码
// 1. 插入单个键值对:返回pair<iterator, bool>
pair<iterator, bool> insert(const value_type& val);

// 2. 插入初始化列表中的键值对
void insert(initializer_list<value_type> il);

// 3. 插入[first, last)区间的键值对
template <class InputIterator>
void insert(InputIterator first, InputIterator last);

3.3.2 关键接口解析(单个键值对插入)

返回值**pair<iterator, bool>**是判断插入结果的核心,其含义如下:

  • first:迭代器,指向 "新插入的键值对"(插入成功)或 "已存在的相同 key 的键值对"(插入失败);
  • second :布尔值,true表示插入成功(key 不存在),false表示插入失败(key 已存在)。

3.3.3 插入键值对的 4 种方式

在插入单个键值对时,可通过以下 4 种方式创建**value_type(pair<const Key, T>)**对象,其中第 3、4 种方式最简洁:

  1. 直接构造pair(显式指定模板参数);
  2. 通过make_pair函数(自动推导类型);
  3. C++11 列表 初始化({key, value});
  4. **直接传入pair**临时对象。

3.3.4 示例:插入操作

cpp 复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    map<int, string> dict;

    // 1. 插入方式1:直接构造pair(显式模板参数)
    pair<int, string> kv1(1, "one");
    auto ret1 = dict.insert(kv1);
    cout << "插入kv1(1, one):" << (ret1.second ? "成功" : "失败") 
         << ",当前key对应的value:" << ret1.first->second << endl;  // 成功,one

    // 2. 插入方式2:make_pair(自动推导类型)
    auto ret2 = dict.insert(make_pair(2, "two"));
    cout << "插入make_pair(2, two):" << (ret2.second ? "成功" : "失败") 
         << ",当前key对应的value:" << ret2.first->second << endl;  // 成功,two

    // 3. 插入方式3:C++11列表初始化(最简洁)
    auto ret3 = dict.insert({3, "three"});
    cout << "插入{3, three}:" << (ret3.second ? "成功" : "失败") 
         << ",当前key对应的value:" << ret3.first->second << endl;  // 成功,three

    // 4. 插入方式4:插入重复key(失败案例)
    auto ret4 = dict.insert({3, "three_new"});
    cout << "插入{3, three_new}:" << (ret4.second ? "成功" : "失败") 
         << ",当前key对应的value:" << ret4.first->second << endl;  // 失败,three

    // 5. 批量插入:初始化列表
    dict.insert({4, "four"}, {5, "five"});  // 错误:需用大括号包裹所有键值对
    dict.insert({{4, "four"}, {5, "five"}});  // 正确:批量插入两个键值对
    cout << "批量插入后dict:" << endl;
    for (const auto& kv : dict) {
        cout << kv.first << ": " << kv.second << " ";
    }
    cout << endl;  // 输出:1: one 2: two 3: three 4: four 5: five 

    return 0;
}

3.4 查找操作(find/count/lower_bound/upper_bound)

查找是 map 的核心优势,通过 key 快速定位 value,支持精确查找、计数查找与范围查找。

3.4.1 查找接口

cpp 复制代码
// 1. 精确查找:返回指向key的迭代器,未找到返回end()
iterator find(const key_type& k);
const_iterator find(const key_type& k) const;

// 2. 计数查找:返回key的个数(map中仅0或1,multimap中为实际个数)
size_type count(const key_type& k) const;

// 3. 范围查找:返回第一个>=k的迭代器
iterator lower_bound(const key_type& k);
const_iterator lower_bound(const key_type& k) const;

// 4. 范围查找:返回第一个>k的迭代器
iterator upper_bound(const key_type& k);
const_iterator upper_bound(const key_type& k) const;

3.4.2 接口使用场景

  • find :最常用的精确查找,需判断返回的迭代器是否为end()
  • count :仅用于判断 key 是否存在(map 中返回 0 或 1),效率与find一致(O (log N));
  • lower_bound/upper_bound:配合使用可获取 [key1, key2] 区间的键值对,适用于范围查询场景。

3.4.3 示例:查找操作

cpp 复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    map<int, string> dict = {
        {1, "one"}, {2, "two"}, {3, "three"}, 
        {4, "four"}, {5, "five"}
    };

    // 1. find精确查找
    int targetKey = 3;
    auto findIt = dict.find(targetKey);
    if (findIt != dict.end()) {
        cout << "找到key=" << targetKey << ",value=" << findIt->second << endl;  // 找到,three
    } else {
        cout << "未找到key=" << targetKey << endl;
    }

    targetKey = 6;
    findIt = dict.find(targetKey);
    if (findIt != dict.end()) {
        cout << "找到key=" << targetKey << ",value=" << findIt->second << endl;
    } else {
        cout << "未找到key=" << targetKey << endl;  // 未找到
    }

    // 2. count计数查找
    targetKey = 4;
    size_t count = dict.count(targetKey);
    cout << "key=" << targetKey << "的个数:" << count << endl;  // 1

    targetKey = 7;
    count = dict.count(targetKey);
    cout << "key=" << targetKey << "的个数:" << count << endl;  // 0

    // 3. lower_bound/upper_bound范围查找(查找key在[2,4]区间的键值对)
    auto lowIt = dict.lower_bound(2);  // 第一个>=2的迭代器(key=2)
    auto upIt = dict.upper_bound(4);   // 第一个>4的迭代器(key=5)
    cout << "key在[2,4]区间的键值对:" << endl;
    for (auto it = lowIt; it != upIt; ++it) {
        cout << it->first << ": " << it->second << " ";
    }
    cout << endl;  // 输出:2: two 3: three 4: four 

    return 0;
}

3.5 删除操作(erase)

删除操作用于移除 map 中的键值对,支持按 "迭代器位置"、"key 值" 或 "迭代器区间" 删除,接口简洁且高效。

3.5.1 删除接口

cpp 复制代码
// 1. 按迭代器位置删除:返回删除元素的下一个元素的迭代器
iterator erase(const_iterator position);

// 2. 按key值删除:返回删除的元素个数(map中0或1)
size_type erase(const key_type& k);

// 3. 按迭代器区间删除:返回删除区间的下一个元素的迭代器
iterator erase(const_iterator first, const_iterator last);

3.5.2 关键注意事项

  • 按迭代器删除时,需确保迭代器有效(不为end()),否则会导致未定义行为;
  • 按 key 删除时,若 key 不存在,返回 0 且无任何操作;若存在,返回 1 且删除对应键值对;
  • 按区间删除时,区间需满足[first, last)(左闭右开),且first需在last之前。

3.5.3 示例:删除操作

cpp 复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    map<int, string> dict = {
        {1, "one"}, {2, "two"}, {3, "three"}, 
        {4, "four"}, {5, "five"}
    };

    // 1. 按迭代器位置删除(删除key=3)
    auto posIt = dict.find(3);
    if (posIt != dict.end()) {
        dict.erase(posIt);
        cout << "删除key=3后dict:" << endl;
        for (const auto& kv : dict) {
            cout << kv.first << ": " << kv.second << " ";
        }
        cout << endl;  // 输出:1: one 2: two 4: four 5: five 
    }

    // 2. 按key值删除(删除key=5)
    size_t delCount = dict.erase(5);
    cout << "删除key=5:" << (delCount ? "成功" : "失败") << endl;  // 成功
    cout << "删除key=5后dict:" << endl;
    for (const auto& kv : dict) {
        cout << kv.first << ": " << kv.second << " ";
    }
    cout << endl;  // 输出:1: one 2: two 4: four 

    // 3. 按区间删除(删除key在[2,4]区间)
    auto lowIt = dict.lower_bound(2);  // key=2
    auto upIt = dict.upper_bound(4);   // key=5(已被删除,实际为end())
    dict.erase(lowIt, upIt);
    cout << "删除区间[2,4]后dict:" << endl;
    for (const auto& kv : dict) {
        cout << kv.first << ": " << kv.second << " ";
    }
    cout << endl;  // 输出:1: one 

    return 0;
}

3.6 数据修改:迭代器与 operator [] 的核心用法

map 的数据修改仅针对valuesecond),keyfirst)因是红黑树排序依据而不可修改。修改value主要通过两种方式:迭代器修改operator [] 修改 ,其中operator[]是 map 最具特色的接口,兼具 "插入、查找、修改" 三重功能。

3.6.1 迭代器修改 value

通过find接口获取目标 key 的迭代器后,直接修改it->second即可,适用于 "先查找再修改" 的场景。

示例

cpp 复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    map<string, int> fruitCount = {{"apple", 5}, {"banana", 3}};

    // 查找并修改apple的数量
    auto it = fruitCount.find("apple");
    if (it != fruitCount.end()) {
        it->second += 2;  // 将5改为7
    }

    cout << "修改后fruitCount:" << endl;
    for (const auto& kv : fruitCount) {
        cout << kv.first << ": " << kv.second << " ";
    }
    cout << endl;  // 输出:apple: 7 banana: 3 

    return 0;
}

3.6.2 operator []:多功能复合接口

operator[]是 map 最常用的接口之一,其核心原理基于insert接口实现,具备 "插入、查找、修改" 三重功能。

1. operator [] 的声明与实现原理
cpp 复制代码
// 声明:返回value的引用,可修改
mapped_type& operator[](const key_type& k);

// 内部实现(简化版)
mapped_type& operator[](const key_type& k) {
    // 尝试插入键值对{k, mapped_type()}(mapped_type()为默认值,如int默认0)
    pair<iterator, bool> ret = insert({k, mapped_type()});
    // 返回value的引用(无论插入成功与否,ret.first均指向目标key的迭代器)
    return ret.first->second;
}
2. operator [] 的三重功能
  • 功能 1:插入键值对 :若 key 不存在,插入**{k, 默认值}**并返回 value 的引用;
  • 功能 2:查找 value:若 key 存在,直接返回 value 的引用;
  • 功能 3:修改 value:通过返回的引用直接修改 value。
3. 实战示例:operator [] 的用法
cpp 复制代码
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    map<string, string> dict;

    // 1. 功能1:插入键值对(key不存在)
    dict["insert"];  // 插入{"insert", ""}(string默认空串)
    dict["left"] = "左边";  // 插入{"left", "左边"}
    cout << "插入后dict:" << endl;
    cout << "insert: " << dict["insert"] << endl;  // 输出:insert: 
    cout << "left: " << dict["left"] << endl;      // 输出:left: 左边

    // 2. 功能2:查找value(key存在)
    string value = dict["left"];
    cout << "查找left的value:" << value << endl;  // 输出:左边

    // 3. 功能3:修改value(key存在)
    dict["left"] = "左边、剩余";  // 将"左边"改为"左边、剩余"
    cout << "修改后left的value:" << dict["left"] << endl;  // 输出:左边、剩余

    // 4. 综合示例:统计单词出现次数
    string words[] = {"apple", "banana", "apple", "orange", "apple"};
    map<string, int> wordCount;
    for (const auto& word : words) {
        wordCount[word]++;  // 关键:不存在则插入(默认0)并+1;存在则直接+1
    }
    cout << "单词统计结果:" << endl;
    for (const auto& kv : wordCount) {
        cout << kv.first << ": " << kv.second << "次" << endl;
    }
    /* 输出:
    apple: 3次
    banana: 1次
    orange: 1次
    */

    return 0;
}
4. operator [] 的注意事项
  • 若仅需 "查找" 而不希望 "插入不存在的 key",应使用find接口(operator[]会自动插入不存在的 key,可能导致意外数据);
  • 对于自定义类型的mapped_type,需确保其有默认构造函数(mapped_type()),否则operator[]无法使用;
  • operator[]返回的是mapped_type&,可直接修改 value,若需只读访问,可使用at()接口(C++11 及以上)或find配合const_iterator

四、multimap 容器:支持 key 冗余的键值对映射

multimap 是 map 的 "兄弟容器",底层同样基于红黑树实现,核心差异仅在于支持 key 冗余(即允许存在多个相同的 key)。这一特性导致 multimap 在接口行为与使用场景上与 map 存在显著区别。

4.1 multimap 的核心特性

  1. key 可冗余:允许插入多个相同的 key,每个 key 对应独立的 value;
  2. key 有序 :与 map 一致,按Compare规则排序,相同 key 的键值对相邻存储;
  3. 高效操作 :增删查改的时间复杂度仍为O (log N)
  4. 无 operator [] 接口 :因 key 可冗余,**operator[]**无法确定返回哪个 key 的 value,故不支持;
  5. 迭代器特性 :与 map 一致,**first(key)**为 const 类型,**second(value)**可修改。

4.2 multimap 与 map 的接口差异

multimap 与 map 的大部分接口一致,但因 key 冗余特性,以下接口的行为存在差异:

接口 map(key 唯一) multimap(key 冗余)
insert 返回pair<iterator, bool>,标识插入成功与否 返回iterator,指向新插入的键值对(插入必成功)
find 返回唯一 key 的迭代器,未找到返回 end () 返回中序遍历的第一个匹配 key 的迭代器
count 返回 0 或 1(判断 key 是否存在) 返回匹配 key 的实际个数
erase(按 key) 删除 1 个匹配 key 的键值对,返回 0 或 1 删除所有匹配 key 的键值对,返回删除个数
operator[] 支持(插入、查找、修改) 不支持

4.3 示例:multimap 的使用

cpp 复制代码
#include <iostream>
#include <map>
using namespace std;

int main() {
    // 1. 初始化multimap(插入重复key)
    multimap<int, string> courseMap;
    courseMap.insert({101, "C++"});
    courseMap.insert({101, "Data Structure"});  // 插入相同key=101
    courseMap.insert({102, "Python"});
    courseMap.insert({101, "Algorithm"});        // 再次插入key=101
    cout << "multimap初始化结果:" << endl;
    for (const auto& kv : courseMap) {
        cout << "Course ID: " << kv.first << ", Name: " << kv.second << endl;
    }
    /* 输出(按key升序,相同key相邻):
    Course ID: 101, Name: C++
    Course ID: 101, Name: Data Structure
    Course ID: 101, Name: Algorithm
    Course ID: 102, Name: Python
    */

    // 2. find接口:返回第一个匹配key的迭代器
    auto findIt = courseMap.find(101);
    if (findIt != courseMap.end()) {
        cout << "\n第一个key=101的课程:" << findIt->second << endl;  // 输出:C++
        // 遍历所有key=101的课程
        cout << "所有key=101的课程:" << endl;
        while (findIt != courseMap.end() && findIt->first == 101) {
            cout << findIt->second << " ";
            ++findIt;
        }
        cout << endl;  // 输出:C++ Data Structure Algorithm
    }

    // 3. count接口:统计key的个数
    size_t courseCount = courseMap.count(101);
    cout << "\nkey=101的课程个数:" << courseCount << endl;  // 输出:3

    // 4. erase接口:按key删除所有匹配项
    size_t delCount = courseMap.erase(101);
    cout << "删除key=101的课程个数:" << delCount << endl;  // 输出:3
    cout << "删除后multimap:" << endl;
    for (const auto& kv : courseMap) {
        cout << "Course ID: " << kv.first << ", Name: " << kv.second << endl;
    }
    /* 输出:
    Course ID: 102, Name: Python
    */

    return 0;
}

4.4 multimap 的适用场景

multimap 因支持 key 冗余,主要适用于以下场景:

  1. 一对多映射关系:如 "课程 ID - 课程名称"(一个 ID 对应多个课程)、"用户 ID - 订单信息"(一个用户对应多个订单);
  2. 按 key 分组存储:需将相同 key 的键值对集中存储,便于批量查询;
  3. 有序重复键值对:需维持 key 的有序性,同时允许重复 key(如按时间戳存储的日志,相同时间戳对应多条日志)。

五、map 系列容器的OJ题实战

map 系列凭借 "键值对映射" 与 "高效查找" 特性,在算法题中有着广泛应用。本节将基于一些实战案例,深入解析 map 在环形链表检测、随机链表复制、高频单词统计等场景的使用。

5.1 场景一:随机链表的复制(键值对映射解决指针关联)

题目链接https://leetcode.cn/problems/copy-list-with-random-pointer/description/

题目描述 :给定一个随机链表,每个节点除了有一个next指针指向下一个节点,还有一个random指针指向链表中的任意节点或null。请深拷贝这个链表,返回拷贝后的头节点。

难点 :普通链表复制只需按next顺序拷贝,但随机链表的random指针可能指向未拷贝的节点,难以直接关联。

解题思路(基于 map)

利用 map 的 "键值对映射" 特性,建立 "原节点 - 拷贝节点" 的一一对应关系:

  1. 第一遍遍历原链表,拷贝每个节点的val,并将{原节点, 拷贝节点}存入 map;
  2. 第二遍遍历原链表,通过 map 找到拷贝节点对应的nextrandom指针(原节点的next/random对应的拷贝节点,即map[原节点->next]/map[原节点->random]),完成拷贝链表的指针关联。
代码实现
cpp 复制代码
#include <iostream>
#include <map>
using namespace std;

// 随机链表节点定义
struct Node {
    int val;
    Node* next;
    Node* random;
    Node(int x) : val(x), next(nullptr), random(nullptr) {}
};

class Solution {
public:
    Node* copyRandomList(Node* head) {
        if (head == nullptr) return nullptr;

        map<Node*, Node*> nodeMap;  // 建立原节点到拷贝节点的映射
        Node* copyHead = nullptr;
        Node* copyTail = nullptr;
        Node* cur = head;

        // 第一遍遍历:拷贝节点val,建立映射
        while (cur != nullptr) {
            Node* newNode = new Node(cur->val);
            nodeMap[cur] = newNode;  // 存储{原节点, 拷贝节点}

            // 构建拷贝链表的next指针(初步)
            if (copyTail == nullptr) {
                copyHead = copyTail = newNode;
            } else {
                copyTail->next = newNode;
                copyTail = copyTail->next;
            }

            cur = cur->next;
        }

        // 第二遍遍历:处理拷贝节点的random指针
        cur = head;
        Node* copyCur = copyHead;
        while (cur != nullptr) {
            // 原节点的random对应拷贝节点的random
            if (cur->random != nullptr) {
                copyCur->random = nodeMap[cur->random];
            } else {
                copyCur->random = nullptr;
            }

            cur = cur->next;
            copyCur = copyCur->next;
        }

        return copyHead;
    }
};

// 测试代码:创建随机链表并拷贝
int main() {
    Solution sol;

    // 创建原链表:1 -> 2 -> 3
    Node* head = new Node(1);
    head->next = new Node(2);
    head->next->next = new Node(3);

    // 设置random指针:1->3,2->1,3->null
    head->random = head->next->next;
    head->next->random = head;
    head->next->next->random = nullptr;

    // 拷贝链表
    Node* copyHead = sol.copyRandomList(head);

    // 验证拷贝结果
    Node* cur = copyHead;
    cout << "拷贝链表节点及random:" << endl;
    while (cur != nullptr) {
        cout << "Node val: " << cur->val;
        if (cur->random != nullptr) {
            cout << ", Random val: " << cur->random->val << endl;
        } else {
            cout << ", Random: null" << endl;
        }
        cur = cur->next;
    }
    /* 输出(与原链表一致):
    Node val: 1, Random val: 3
    Node val: 2, Random val: 1
    Node val: 3, Random: null
    */

    // 释放内存(简化处理)
    delete head->next->next;
    delete head->next;
    delete head;
    delete copyHead->next->next;
    delete copyHead->next;
    delete copyHead;

    return 0;
}
复杂度分析
  • 时间复杂度:O (n),其中 n 为链表长度,两次遍历均为 O (n),map 插入与查找均为 O (log n),但整体由遍历主导;
  • 空间复杂度:O (n),用于存储 map 中的 n 个节点映射。

5.2 场景二:前 K 个高频单词(map 统计 + 排序)

题目链接https://leetcode.cn/problems/top-k-frequent-words/description/

题目描述:给定一个单词列表,返回前 K 个出现频率最高的单词。若多个单词频率相同,则按字典序排序(字典序小的在前)。

解题思路

  1. 用 map 统计每个单词的出现频率(利用**operator[]**简化统计);
  2. 将 map 中的键值对转换为 vector,按 "频率降序、字典序升序" 排序;
  3. 取排序后的前 K 个单词作为结果。
代码实现
cpp 复制代码
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <string>
using namespace std;

class Solution {
public:
    // 自定义排序规则:先按频率降序,再按字典序升序
    struct Compare {
        bool operator()(const pair<string, int>& a, const pair<string, int>& b) const {
            if (a.second != b.second) {
                return a.second > b.second;  // 频率降序
            } else {
                return a.first < b.first;   // 字典序升序
            }
        }
    };

    vector<string> topKFrequent(vector<string>& words, int k) {
        // 1. 用map统计单词频率(map默认按key字典序排序,便于后续频率相同时的排序)
        map<string, int> countMap;
        for (const auto& word : words) {
            countMap[word]++;  // 统计频率,key自动按字典序排序
        }

        // 2. 将map转换为vector,便于排序
        vector<pair<string, int>> freqVec(countMap.begin(), countMap.end());

        // 3. 按自定义规则排序
        sort(freqVec.begin(), freqVec.end(), Compare());

        // 4. 取前K个单词作为结果
        vector<string> result;
        for (int i = 0; i < k; ++i) {
            result.push_back(freqVec[i].first);
        }

        return result;
    }
};

// 测试代码
int main() {
    Solution sol;
    vector<string> words = {
        "i", "love", "leetcode", "i", "love", "coding"
    };
    int k = 2;

    vector<string> topK = sol.topKFrequent(words, k);
    cout << "前" << k << "个高频单词:" << endl;
    for (const auto& word : topK) {
        cout << word << " ";
    }
    cout << endl;  // 输出:i love 

    return 0;
}
复杂度分析
  • 时间复杂度:O (n log n),其中 n 为单词总数。map 统计频率为 O (n log m)(m 为不同单词数),排序为 O (m log m),整体为 O (n log n);
  • 空间复杂度:O (m),用于存储 map 和 vector。

总结

map 系列是 C++ STL 中用于键值对存储的核心容器,通过红黑树实现了高效的增删查改操作,通过 "key 唯一"(map)与 "key 冗余"(multimap)的特性覆盖了不同场景的需求。掌握 map 系列的使用,能大幅提升代码的结构化程度与效率,尤其在字典查询、数据统计、缓存映射等场景中不可或缺。在实际开发中,需根据 "是否允许 key 冗余""是否需要有序""查找效率要求" 等因素,选择 map、multimap 或 unordered_map,让工具更好地服务于需求。

相关推荐
hetao17338371 小时前
ZYZ28-NOIP模拟赛-Round4 hetao1733837的record
c++·算法
大米粥哥哥1 小时前
c++ libcurl报错Send failed since rewinding of the data stream failed【已解决】
开发语言·c++·http·curl·rewind
Nebula_g1 小时前
C语言应用实例:解方程(二分查找)
c语言·开发语言·学习·算法·二分查找·基础
woshimyc1 小时前
ESP32连接ThingsCloud上传设备数据(智慧小灯)
c++·物联网
散峰而望1 小时前
C语言刷题-编程(一)(基础)
c语言·开发语言·编辑器
Dxxyyyy2 小时前
零基础学JAVA--Day27(注释+异常+异常处理方法)
java·开发语言
视图猿人2 小时前
RxJS基本使用及在next.js中使用的例子
开发语言·javascript
Maple_land2 小时前
Linux复习:系统调用与fork
linux·运维·服务器·c++·centos
墨雪不会编程2 小时前
C++的基础语法篇一 ——命名空间
开发语言·c++