【C++进阶】关联容器:multimap类型

news2025/4/18 1:04:00

目录

一、multimap 基础概念与底层实现

1.1 定义与核心特性

1.2 底层数据结构

1.3 类模板定义

1.4 与其他容器的对比

二、multimap 核心操作详解

2.1 定义与初始化

2.2 插入元素

2.3 查找元素

2.4 删除元素

2.5 遍历元素

三、性能分析与适用场景

3.1 时间复杂度分析

3.2 适用场景

四、高级应用与进阶技巧

4.1 自定义比较函数

4.2 处理 “一对多” 关系

4.3 性能优化建议

4.4 与 map 的协同使用

五、常见问题与陷阱

5.1 为什么没有 operator []?

5.2 自定义比较函数的严格弱序

5.3 迭代器失效问题

六、实战案例:文件关键词索引系统

6.1 需求描述

6.2 代码实现

七、总结与最佳实践

7.1 使用场景总结

7.2 最佳实践

7.3 未来扩展

八、完整代码示例

九、结语

十、参考资料


在 C++ 标准库的关联容器中,multimap 是一种特殊的存在。它允许键(Key)重复,能够存储多个具有相同键的键值对,同时保持键的有序性。这种特性使得 multimap 在处理 “一对多” 关系(如课程与学生、标签与文章)时高效且便捷。

一、multimap 基础概念与底层实现

1.1 定义与核心特性

multimap 是 map 的 “兄弟” 容器,二者的核心区别在于:

  • 键的唯一性map 要求键唯一,multimap 允许键重复
  • 插入行为map 插入重复键时会被忽略,multimap 会直接插入新元素
  • 操作接口multimap 不提供 operator[](因键不唯一,无法直接通过键访问唯一值)

典型应用场景

  • 学生管理系统:同一个班级(键)对应多个学生(值)
  • 日志系统:同一个时间戳(键)对应多条日志记录(值)
  • 索引系统:同一个关键词(键)对应多个文档编号(值)

1.2 底层数据结构

multimap 与 map 一样,底层基于 红黑树(Red-Black Tree) 实现。红黑树是平衡二叉搜索树,保证插入、删除、查找的时间复杂度均为 O(log n)。与 map 的区别在于,multimap 允许红黑树节点存储相同键的元素,插入时不会检查键是否已存在,而是直接按顺序插入到合适位置(保持中序遍历的有序性)。

红黑树节点结构示意图:

1.3 类模板定义

template <
    class Key,
    class T,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<std::pair<const Key, T>>
> class multimap;

1.4 与其他容器的对比

容器键唯一性有序性底层结构查找时间典型场景
map唯一有序红黑树O(log n)一对一映射
multimap允许重复有序红黑树O(log n)一对多映射(有序)
unordered_map唯一无序哈希表O (1)(平均)高速查找(无需顺序)
unordered_multimap允许重复无序哈希表O (1)(平均)高速一对多映射(无序)

二、multimap 核心操作详解

2.1 定义与初始化

#include <iostream>
#include <map>
#include <string>

// 基本定义:键为 int,值为 string
std::multimap<int, std::string> mm;

// 初始化方式 1:插入多个键值对
std::multimap<int, std::string> scores = {
    {100, "Alice"},
    {100, "Bob"},    // 键重复,合法
    {200, "Charlie"}
};

// 初始化方式 2:通过迭代器范围
std::multimap<int, std::string> copyScores(scores.begin(), scores.end());

2.2 插入元素

multimap 提供多种插入方式,均允许键重复:

方式一:insert 函数

// 插入单个键值对(推荐)
mm.insert(std::make_pair(300, "David"));
mm.insert({300, "Eve"});  // C++11 统一初始化语法

// 插入另一个 multimap 的元素
mm.insert(copyScores.begin(), copyScores.end());

方式二:emplace 就地构造

// 直接构造对象,避免临时对象拷贝
mm.emplace(400, "Frank");

2.3 查找元素

由于键可能重复,multimap 的查找需返回键的范围:

方法一:find 查找第一个匹配键 

auto it = mm.find(100);
if (it != mm.end()) {
    std::cout << "First entry for key 100: " << it->second << std::endl;  // 输出 Alice
}

方法二:equal_range 查找键的完整范围

auto range = mm.equal_range(100);
for (auto it = range.first; it != range.second; ++it) {
    std::cout << "Key 100 value: " << it->second << std::endl;
}
// 输出:
// Key 100 value: Alice
// Key 100 value: Bob

方法三:lower_bound 和 upper_bound

auto lower = mm.lower_bound(100);   // 第一个 >= 100 的键
auto upper = mm.upper_bound(100);   // 第一个 > 100 的键
for (auto it = lower; it != upper; ++it) {
    // 遍历键为 100 的所有元素
}

2.4 删除元素

删除指定键的所有元素

size_t count = mm.erase(100);  // 返回删除的元素个数(此处为 2)
std::cout << "Erased " << count << " entries for key 100" << std::endl;

删除单个元素(通过迭代器)

auto it = mm.find(200);
if (it != mm.end()) {
    mm.erase(it);  // 删除键为 200 的第一个元素
}

2.5 遍历元素

// 正向遍历(按键的升序排列)
for (const auto& entry : mm) {
    std::cout << entry.first << ": " << entry.second << std::endl;
}

// 反向遍历(按键的降序排列)
for (auto it = mm.rbegin(); it != mm.rend(); ++it) {
    std::cout << it->first << ": " << it->second << std::endl;
}

三、性能分析与适用场景

3.1 时间复杂度分析

操作平均复杂度最坏复杂度
插入(insert)O(log n)O(n)
删除(erase)O(log n)O(n)
查找(find)O(log n)O(n)

注意:红黑树的性能在极端情况下可能退化为O(n),但通过自动平衡机制,这种情况发生的概率极低。

3.2 适用场景

  • 一对多关系存储:如学生成绩管理(同名学生不同分数)、电话簿(同名多号码)。
  • 范围查询需求:需要频繁按键范围查询数据(如时间区间内的日志记录)。
  • 有序性要求:数据需要按键自动排序,且支持高效插入/删除。

对比其他容器

  • map:键唯一,适合一对一映射。
  • unordered_multimap:基于哈希表,无序但平均O(1)查找,适合高频查询场景。
  • vector/list:无序容器,需手动维护排序。

四、高级应用与进阶技巧

4.1 自定义比较函数

默认情况下,multimap 使用 std::less<Key> 进行键的排序。若需自定义排序规则(如降序、自定义结构体比较),需在声明时指定比较函子。

示例:降序排序

struct DescCompare {
    bool operator()(int a, int b) const {
        return a > b;  // 降序排列
    }
};

std::multimap<int, std::string, DescCompare> reversedMm;
reversedMm.insert({100, "A"});
reversedMm.insert({200, "B"});
// 遍历顺序:200, 100

示例:结构体键的比较

struct Student {
    int grade;
    std::string name;
    // 自定义比较:先按成绩降序,同成绩按姓名升序
    bool operator<(const Student& other) const {
        if (grade != other.grade) {
            return grade > other.grade;
        }
        return name < other.name;
    }
};

// 使用 Student 作为键类型
std::multimap<Student, int> studentScores;
studentScores.insert({{90, "Alice"}, 1001});
studentScores.insert({{90, "Bob"}, 1002});  // 键相同,允许插入

4.2 处理 “一对多” 关系

multimap 的核心价值在于高效存储同一键的多个值,典型场景如下:

场景:课程与学生管理

// 课程编号为键,学生姓名为值
std::multimap<int, std::string> courseStudents;

// 插入数据:课程 101 有多个学生
courseStudents.insert({101, "Alice"});
courseStudents.insert({101, "Bob"});
courseStudents.insert({102, "Charlie"});

// 查询课程 101 的所有学生
auto [lower, upper] = courseStudents.equal_range(101);
std::cout << "Course 101 students:" << std::endl;
for (auto it = lower; it != upper; ++it) {
    std::cout << "- " << it->second << std::endl;
}

4.3 性能优化建议

  • 批量插入:使用 insert 插入迭代器范围或初始化列表,减少红黑树旋转次数

  • 预分配内存:通过 reserve 预分配空间(虽然红黑树无容量概念,但可减少重新分配次数)

  • 避免频繁修改键:键是 const 类型,修改键需先删除旧元素再插入新元素 

4.4 与 map 的协同使用

当需要 “键 - 值列表” 映射时,可使用 map<Key, vector<Value>>,但 multimap 在插入时更便捷:

// 使用 map + vector 实现一对多
std::map<int, std::vector<std::string>> mapWithVector;
mapWithVector[100].push_back("Alice");
mapWithVector[100].push_back("Bob");  // 需要手动管理 vector

// 使用 multimap 更简洁
std::multimap<int, std::string> mm;
mm.insert({100, "Alice"});
mm.insert({100, "Bob"});  // 直接插入,无需预分配 vector

五、常见问题与陷阱

5.1 为什么没有 operator []?

map 的 operator[] 通过键访问唯一值,而 multimap 中键可能对应多个值,无法通过单一值返回,因此未提供该接口。若需模拟类似功能,需手动插入: 

// map 的用法(唯一值)
map[100] = "Alice";  // 若键不存在则插入,存在则覆盖

// multimap 需用 insert
mm.insert({100, "Alice"});  // 允许重复插入

5.2 自定义比较函数的严格弱序

比较函数必须满足 严格弱序(Strict Weak Ordering)

  • 非自反性:!(a < a)
  • 传递性:若 a < b 且 b < c,则 a < c
  • 反对称性:若 a < b,则 b < a 不成立

错误示例(非严格弱序,导致未定义行为):

// 错误:比较函数不满足非自反性
struct BadCompare {
    bool operator()(int a, int b) const { return a <= b; }  // a <= b 允许 a==b,导致自反
};

5.3 迭代器失效问题

  • 插入、删除操作不会使其他迭代器失效(红黑树节点替换而非销毁)
  • 但删除元素后,指向该元素的迭代器会失效

六、实战案例:文件关键词索引系统

6.1 需求描述

实现一个学生课程管理系统,支持:

  • 添加学生选课记录(允许同一学生选择多门课程)。
  • 按学生姓名查询所有选课记录。
  • 按课程名称查询所有选课学生。

6.2 代码实现

#include <iostream>
#include <map>
#include <string>

int main() {
    // 使用multimap存储学生-课程关系(允许重复)
    std::multimap<std::string, std::string> studentCourses;

    // 添加选课记录
    studentCourses.insert({"Alice", "Math"});
    studentCourses.insert({"Alice", "Physics"});
    studentCourses.insert({"Bob", "Chemistry"});
    studentCourses.insert({"Alice", "Biology"});

    // 按学生查询课程
    std::string targetStudent = "Alice";
    auto range = studentCourses.equal_range(targetStudent);
    std::cout << targetStudent << "'s courses:" << std::endl;
    for (auto it = range.first; it != range.second; ++it) {
        std::cout << "- " << it->second << std::endl;
    }

    // 按课程查询学生(需反向multimap)
    std::multimap<std::string, std::string> courseStudents;
    for (const auto& pair : studentCourses) {
        courseStudents.insert({pair.second, pair.first});
    }

    std::string targetCourse = "Physics";
    auto courseRange = courseStudents.equal_range(targetCourse);
    std::cout << "\nStudents in " << targetCourse << ":" << std::endl;
    for (auto it = courseRange.first; it != courseRange.second; ++it) {
        std::cout << "- " << it->second << std::endl;
    }

    return 0;
}

 

七、总结与最佳实践

7.1 使用场景总结

  • 键可重复且需有序:如日志系统、索引系统、分类系统
  • 简化 “一对多” 操作:避免手动管理 map + vector 的嵌套结构
  • 范围查询需求:利用 lower_bound/upper_bound 进行高效区间操作

7.2 最佳实践

  • 优先使用 insert 插入:避免依赖 operator[]multimap 不支持)
  • 处理重复键时使用范围迭代:通过 equal_range 或 lower_bound/upper_bound 遍历所有相关元素
  • 自定义比较函数时严格遵循严格弱序:确保红黑树的有序性不受破坏
  • 对比选择容器
    • 若需要键唯一且有序:用 map
    • 若需要键可重复但无需有序:用 unordered_multimap
    • 若需要值可重复且键值相同:用 multiset

7.3 未来扩展

C++ 标准库不断进化,虽然 multimap 的接口相对稳定,但结合 Lambda 表达式可更简洁地定义比较函数(C++14 起):

// 使用 Lambda 作为比较函数(C++14+)
auto lambdaCompare = [](int a, int b) { return a > b; };
std::multimap<int, std::string, decltype(lambdaCompare)> mm(lambdaCompare);

八、完整代码示例

示例 1:基本操作演示

#include <iostream>
#include <map>
#include <string>

int main() {
    std::multimap<int, std::string> mm;

    // 插入元素
    mm.insert({100, "Alice"});
    mm.insert({100, "Bob"});
    mm.insert({200, "Charlie"});

    // 查找键 100 的所有值
    auto range = mm.equal_range(100);
    std::cout << "Values for key 100:" << std::endl;
    for (auto it = range.first; it != range.second; ++it) {
        std::cout << "- " << it->second << std::endl;
    }

    // 删除键 100 的所有元素
    mm.erase(100);
    std::cout << "Size after erase: " << mm.size() << std::endl;  // 输出 1

    return 0;
}

 

示例 2:自定义比较函数

#include <iostream>
#include <map>

struct DescCompare {
    bool operator()(int a, int b) const { return a > b; }
};

int main() {
    std::multimap<int, std::string, DescCompare> reversedMm;
    reversedMm.insert({100, "A"});
    reversedMm.insert({200, "B"});

    // 反向遍历输出(实际存储顺序为 200, 100)
    for (const auto& entry : reversedMm) {
        std::cout << entry.first << ": " << entry.second << std::endl;
    }

    return 0;
}

 

九、结语

multimap 是处理 “有序重复键” 场景的利器,其红黑树底层保证了高效的插入、删除和查找操作。通过合理使用范围查询接口和自定义比较函数,能轻松构建复杂的数据映射系统。在实际项目中,需根据键的唯一性、有序性和性能需求,灵活选择 mapmultimapunordered_map 等容器,以实现代码的高效与优雅。

十、参考资料

  •  《C++ Primer(第 5 版)》这本书是 C++ 领域的经典之作,对 C++ 的基础语法和高级特性都有深入讲解。
  • 《Effective C++(第 3 版)》书中包含了很多 C++ 编程的实用建议和最佳实践。
  • 《C++ Templates: The Complete Guide(第 2 版)》该书聚焦于 C++ 模板编程,而using声明在模板编程中有着重要应用,如定义模板类型别名等。
  • C++ 官方标准文档:C++ 标准文档是最权威的参考资料,可以查阅最新的 C++ 标准(如 C++11、C++14、C++17、C++20 等)文档。例如,ISO/IEC 14882:2020 是 C++20 标准的文档,可从相关渠道获取其详细内容。
  • :这是一个非常全面的 C++ 在线参考网站,提供了详细的 C++ 语言和标准库文档。
  • :该网站提供了系统的 C++ 教程,配有丰富的示例代码和清晰的解释,适合初学者学习和理解相关知识。
  • 《Effective STL》Scott Meyers

  • 开源项目STL源码分析


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2332262.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

远程管理命令:关机和重启

关机/重启 序号命令对应英文作用01shutdown 选项 时间shutdown关机 / 重新启动 一、shutdown shutdown 命令可以安全关闭 或者 重新启动系统。 选项含义-r重新启动 提示&#xff1a; 不指定选项和参数&#xff0c;默认表示 1 分钟之后 关闭电脑远程维护服务器时&#xff0…

【MySQL】001.MySQL安装

文章目录 一. MySQL在Ubuntu 20.04 环境安装1.1 更新软件包列表1.2 安装MySQL服务器1.3 配置安全设置1.4 检查mysql server是否正在运行1.5 进行连接1.6 查询自带的数据库 二. 配置文件的修改三. MySQL连接TCP/IP时的登陆问题四. MySQL中的命令 一. MySQL在Ubuntu 20.04 环境安…

vue 入门:组件事件

文章目录 vue介绍vue 入门简单示例自定义组件事件 vue介绍 vue2 官网 Vue (读音 /vjuː/&#xff0c;类似于 view) 是一套用于构建用户界面的渐进式框架。Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层。 vue 入门 Vue.js 的核心是一个允许采用简洁的模板语…

数据质量问题中,数据及时性怎么保证?如何有深度体系化回答!

数据治理&#xff0c;数据质量这快是中大厂&#xff0c;高阶大数据开发面试必备技能&#xff0c;企业基于大数据底座去做数仓&#xff0c;那么首先需要保障的就是数据质量。 数据质量的重要性在现代企业中变得越发突出。以下是数据质量的几个关键方面&#xff0c;说明其对企业…

数据可视化 —— 折线图应用(大全)

一、导入需要的库 # Matplotlib 是 Python 最常用的绘图库&#xff0c;pyplot 提供了类似 MATLAB 的绘图接口 import matplotlib.pyplot as plt import numpy as np import pandas as pd 二、常用的库函数 plt.plot(x轴,y轴)&#xff1a;plot()是画折线图的函数。 plt.xlabe…

什么是中性线、零线、地线,三相四线制如何入户用电

在变压器三相电侧&#xff0c;按照星形连接法&#xff0c;有一个中心点&#xff0c;这根线引出来的线接不接地&#xff1a;不接地就是中性线&#xff0c;接地就是零线 下面就是没有接地&#xff1a;中性线 接地了以后就可以叫做零线了 三相电在高压输电的时候是没有零线的&a…

【含文档+PPT+源码】基于Android家政服务系统的开发与实现

介绍视频&#xff1a; 课程简介&#xff1a; 本课程演示的是一款基于Android家政服务系统的开发与实现&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 1.包含&#xff1a;项目源码、项目文档、数据库脚本、软件工具等所有资料 2.…

配置mac mini M4 的一些软件

最近更换了 mac mini M4 &#xff0c;想要重新下载配置软件 &#xff0c;记录一下。 Homebrew是什么&#xff1f; homebrew是一款Mac OS平台下的软件包管理工具&#xff0c;拥有安装、卸载、更新、查看、搜索等功能。通过简单的指令可以实现包管理&#xff0c;而不用关心各种…

Java——抽象方法抽象类 接口 详解及综合案例

1.抽象方法抽象类 介绍 抽象方法: 将共性的行为(方法)抽取到父类之后&#xff0c; 由于每一个子类执行的内容是不一样&#xff0c; 所以&#xff0c;在父类中不能确定具体的方法体。 该方法就可以定义为抽象方法。 抽象类: 如果一个类中存在抽象方法&#xff0c;那么该类就必须…

【计网】一二章习题

1. (单选题, 3 分) 假设主机A和B之间的链路带宽为100Mbps&#xff0c;主机A的网卡速率为1Gbps&#xff0c;主机B的网卡速率为10Mbps&#xff0c;主机A给主机B发送数据的最高理论速率为&#xff08; &#xff09;。 A. 100Mbps B. 1Gbps C. 1Mbps D. 10Mbps 正确答案 D 发…

【软考-高级】【信息系统项目管理师】【论文基础】进度管理过程输入输出及工具技术的使用方法

定义 项目进度管理是为了保证项目按时完成&#xff0c;对项目中所需的各个过程进行管理的过程&#xff0c;包括规划进度、定义活动、活动优先级排序、活动持续时间、制定进度计划和控制进度。 管理基础 制定进度计划的一般步骤 选择进度计划方法&#xff08;如关键路径法&a…

TOGAF之架构标准规范-技术架构

TOGAF是工业级的企业架构标准规范&#xff0c;本文主要描述技术架构阶段。 如上所示&#xff0c;技术架构&#xff08;Technology Architecture&#xff09;在TOGAF标准规范中处于D阶段 技术架构阶段 技术架构阶段的主要内容包括阶段目标、阶段输入、流程步骤、阶段输出、架构…

Ansys Electronics 变压器 ACT

你好&#xff0c; 在本博客中&#xff0c;我将讨论如何使用 Ansys 电子变压器 ACT 自动快速地设计电力电子电感器或变压器。我将逐步介绍设计和创建电力电子变压器示例的步骤&#xff0c;该变压器为同心组件&#xff0c;双绕组&#xff0c;采用正弦电压激励&#xff0c;并应用…

十三种物联网/通信模块综合对比——《数据手册--物联网/通信模块》

物联网&#xff0f;通信模块 名称 功能 应用场景 USB转换模块 用于将USB接口转换为其他类型的接口&#xff0c;如串口、并口等&#xff0c;实现不同设备之间的通信。 常用于计算机与外部设备&#xff08;如打印机、扫描仪等&#xff09;的连接&#xff0c;以及数据传输和设…

Redis安装(Windows环境)

文章目录 Resid简介:下载Redis启动Redis服务设置Windows服务常用的Redis服务命令 Resid简介: Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库&#xff0c;并提供多种语言的 API。 Redis通常…

FreeRTOS项目工程完善指南:STM32F103C8T6系列

FreeRTOS项目工程完善指南&#xff1a;STM32系列 本文是FreeRTOS STM32开发系列教程的一部分。我们将完善之前移植的FreeRTOS工程&#xff0c;添加串口功能并优化配置文件。 更多优质资源&#xff0c;请访问我的GitHub仓库&#xff1a;https://github.com/Despacito0o/FreeRTO…

论坛系统(测试报告)

文章目录 一、项目介绍二、设计测试用例三、自动化测试用例的部分展示用户名或密码错误登录成功编辑自己的帖子成功修改个人信息成功回复帖子信息成功 四、性能测试总结 一、项目介绍 本平台是用Java开发&#xff0c;基于SpringBoot、SpringMVC、MyBatis框架搭建的小型论坛系统…

【汽车产品开发项目管理——端到端的汽车产品诞生流程】

MPU&#xff1a;集成运算器、寄存器和控制器的中央处理器芯片 MCU&#xff1a;微控制单元&#xff0c;将中央处理器CPU、存储器ROM/RAM、计数器、IO接口及多种外设模块集成在单一芯片上的微型计算机系统。 汽车产品开发项目属性&#xff1a;临时性、独特性、渐进明细性、以目标…

从零到有的游戏开发(visual studio 2022 + easyx.h)

引言 本文章适用于C语言初学者掌握基本的游戏开发&#xff0c; 我将用详细的步骤引领大家如何开发属于自己的游戏。 作者温馨提示&#xff1a;不要认为开发游戏很难&#xff0c;一些基本的游戏逻辑其实很简单&#xff0c; 关于游戏的开发环境也不用担心&#xff0c;我会详细…

【C++初阶】--- vector容器功能模拟实现

1.什么是vector&#xff1f; 在 C 里&#xff0c;std::vector 是标准模板库&#xff08;STL&#xff09;提供的一个非常实用的容器类&#xff0c;它可以看作是动态数组 2.成员变量 iterator _start;&#xff1a;指向 vector 中第一个元素的指针。 iterator _finish;&#x…