C++ multimap 全面解析与实战指南

C++ multimap 全面解析与实战指南

在C++标准模板库(STL)的关联容器中,multimap是一种支持"一对多"映射关系的有序容器。它与map的核心区别在于允许键(key)重复,这使得它在处理需要多值映射的场景(如索引、分组等)时极具优势。本文将从multimap的底层实现原理出发,详细讲解其常用接口、核心特性,结合实战案例演示具体用法,并对比map说明适用场景,帮助大家彻底掌握这一实用容器。

一、multimap 核心原理与特性

要理解multimap的行为,首先需要明确其底层实现和核心特性。multimap与map、set、multiset同属关联容器,底层均基于红黑树(一种自平衡的二叉搜索树)实现。这种数据结构确保了容器内元素的有序性(默认按key升序排列),同时保证了插入、删除、查找等操作的时间复杂度为O(log n)。

1.1 核心特性总结

  • 键值对(key-value)存储,支持键重复(一个key可对应多个value);

  • 元素默认按key的升序排列(可通过自定义比较函数修改排序规则);

  • 底层红黑树实现,插入、删除、查找操作效率稳定(O(log n));

  • 不支持通过key直接修改value(key是排序的依据,修改会破坏有序性);

  • 不支持operator[]操作符(因key重复,无法唯一定位元素);

  • 迭代器为双向迭代器,支持遍历,但不支持随机访问。

1.2 与map的核心区别

很多开发者会混淆map和multimap,两者底层实现完全一致,核心差异仅在于对key唯一性的要求:

特性 map multimap
key唯一性 key不可重复,插入重复key会失败 key可重复,支持一个key对应多个value
operator[] 支持,可通过key直接访问/插入元素 不支持,因key重复无法唯一定位
插入返回值 返回pair<iterator, bool>,标记插入成功与否 仅返回插入位置的迭代器(插入必成功)
查找功能 find()返回唯一匹配的元素迭代器 find()返回第一个匹配key的元素迭代器,需结合equal_range()获取所有匹配元素

二、C++ multimap 常用接口详解

使用multimap前,需包含头文件 <map>(与map共用头文件),并使用std命名空间(或显式指定std::multimap)。multimap的接口与map大部分一致,以下重点讲解核心接口及独有的多值处理接口。

2.1 构造与析构

接口原型 功能说明 示例
multimap(); 默认构造函数,创建空multimap(默认key升序) std::multimap<int, std::string> mm;
multimap(const Compare& comp); 自定义比较函数构造空multimap(如降序) std::multimap<int, std::string, std::greater> mm;
multimap(InputIterator first, InputIterator last); 迭代器构造,拷贝[first, last)区间的键值对 std::multimap<int, std::string> mm2(mm.begin(), mm.end());
multimap(const multimap& other); 拷贝构造函数 std::multimap<int, std::string> mm3(mm);
~multimap(); 析构函数,释放所有资源(红黑树节点) -

2.2 迭代器相关

multimap的迭代器为双向迭代器,支持++/--操作,可用于遍历容器内的有序元素。需要注意:修改迭代器指向的元素时,不能修改key(会破坏红黑树的有序性),但可以修改value。

接口 功能说明
begin() / end() 返回指向第一个元素/最后一个元素下一个位置的迭代器(非const)
rbegin() / rend() 返回指向最后一个元素/第一个元素前一个位置的反向迭代器
cbegin() / cend() 返回const迭代器,不可修改元素(key和value均不可改)
迭代器遍历示例:
cpp 复制代码
#include <map>
#include <iostream>
#include <string>
using namespace std;

int main() {
    // 构造multimap,key为int,value为string,默认升序
    multimap<int, string> mm = {{1, "苹果"}, {2, "香蕉"}, {1, "橙子"}, {3, "葡萄"}};
    
    // 正向遍历(按key升序)
    cout << "正向遍历:" << endl;
    for (auto it = mm.begin(); it != mm.end(); ++it) {
        // it指向pair<const int, string>,key为const不可改
        cout << "key: " << it->first << ", value: " << it->second << endl;
    }
    // 输出:
    // key: 1, value: 苹果
    // key: 1, value: 橙子
    // key: 2, value: 香蕉
    // key: 3, value: 葡萄
    
    // 反向遍历(按key降序)
    cout << "\n反向遍历:" << endl;
    for (auto it = mm.rbegin(); it != mm.rend(); ++it) {
        cout << "key: " << it->first << ", value: " << it->second << endl;
    }
    return 0;
}

2.3 容量相关

接口 功能说明
size() 返回当前键值对的个数
empty() 判断容器是否为空(空返回true)
max_size() 返回容器可容纳的最大键值对个数(受系统内存限制)

2.4 插入与删除(核心操作)

multimap支持多种插入方式,且因key可重复,插入操作始终成功(只要内存足够)。删除操作可按key删除、按迭代器删除,灵活性较高。

接口 功能说明 时间复杂度
insert(const value_type& val) 插入键值对val,返回插入位置的迭代器 O(log n)
insert(InputIterator first, InputIterator last) 插入[first, last)区间的键值对 O(k log(n+k))(k为区间元素个数)
emplace(Args&&... args) 直接构造键值对(避免拷贝,效率更高) O(log n)
erase(iterator pos) 删除迭代器pos指向的键值对,返回下一个元素的迭代器 O(log n)
erase(const key_type& key) 删除所有key匹配的键值对,返回删除的元素个数 O(log n + k)(k为匹配的元素个数)
erase(iterator first, iterator last) 删除[first, last)区间的键值对,返回下一个元素的迭代器 O(log n + k)(k为区间元素个数)
clear() 清空容器,删除所有键值对(size变为0) O(n)
插入与删除示例:
cpp 复制代码
#include <map>
#include <iostream>
#include <string>
using namespace std;

int main() {
    multimap<int, string> mm;
    
    // 1. 插入单个键值对
    mm.insert({1, "苹果"}); // 用初始化列表
    mm.insert(pair<int, string>(1, "橙子")); // 用pair构造
    mm.emplace(2, "香蕉"); // emplace直接构造(更高效)
    
    cout << "插入后size: " << mm.size() << endl; // 输出:3
    
    // 2. 按key删除(删除所有key=1的元素)
    size_t delCount = mm.erase(1);
    cout << "删除key=1的元素个数: " << delCount << endl; // 输出:2
    cout << "删除后size: " << mm.size() << endl; // 输出:1
    
    // 3. 按迭代器删除
    auto it = mm.begin();
    mm.erase(it);
    cout << "按迭代器删除后size: " << mm.size() << endl; // 输出:0
    
    // 4. 清空容器
    mm.insert({3, "葡萄"}, {4, "芒果"});
    mm.clear();
    cout << "clear后empty: " << mm.empty() << endl; // 输出:1(true)
    
    return 0;
}

2.5 查找与统计(核心操作)

由于multimap支持key重复,查找操作除了常规的find(),还提供了专门用于获取同一key所有对应元素的接口(equal_range()、lower_bound()、upper_bound()),这是multimap的核心优势所在。

接口 功能说明 返回值
find(const key_type& key) 查找key匹配的第一个元素 匹配元素的迭代器;若无匹配,返回end()
count(const key_type& key) 统计key匹配的元素个数 匹配元素的个数(无匹配返回0)
lower_bound(const key_type& key) 查找第一个key >= 目标key的元素 对应迭代器;若无则返回end()
upper_bound(const key_type& key) 查找第一个key > 目标key的元素 对应迭代器;若无则返回end()
equal_range(const key_type& key) 获取所有key == 目标key的元素区间 pair<iterator, iterator>,first为lower_bound()结果,second为upper_bound()结果
查找与统计示例(核心重点):
cpp 复制代码
#include <map>
#include <iostream>
#include <string>
using namespace std;

int main() {
    multimap<int, string> mm = {{1, "苹果"}, {2, "香蕉"}, {1, "橙子"}, {3, "葡萄"}, {1, "草莓"}};
    
    // 1. 查找key=1的第一个元素
    auto findIt = mm.find(1);
    if (findIt != mm.end()) {
        cout << "find key=1: " << findIt->first << " - " << findIt->second << endl;
    } // 输出:find key=1: 1 - 苹果
    
    // 2. 统计key=1的元素个数
    size_t count = mm.count(1);
    cout << "key=1的元素个数: " << count << endl; // 输出:3
    
    // 3. 用lower_bound和upper_bound获取key=1的所有元素
    auto lowIt = mm.lower_bound(1);
    auto upIt = mm.upper_bound(1);
    cout << "\nlower_bound & upper_bound遍历key=1的元素:" << endl;
    for (auto it = lowIt; it != upIt; ++it) {
        cout << it->first << " - " << it->second << endl;
    }
    // 输出:
    // 1 - 苹果
    // 1 - 橙子
    // 1 - 草莓
    
    // 4. 用equal_range获取key=1的所有元素(更简洁)
    auto range = mm.equal_range(1);
    cout << "\nequal_range遍历key=1的元素:" << endl;
    for (auto it = range.first; it != range.second; ++it) {
        cout << it->first << " - " << it->second << endl;
    } // 输出与上面一致
    
    // 5. 查找不存在的key
    auto noIt = mm.find(4);
    if (noIt == mm.end()) {
        cout << "\nkey=4不存在" << endl;
    }
    
    return 0;
}

三、multimap 实战案例

multimap的核心应用场景是"多值映射",以下通过两个经典案例演示其实际用法:

3.1 场景1:学生成绩分组(按班级索引)

需求:存储多个班级学生的成绩,按班级号分组,支持查询某个班级的所有学生成绩。

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

// 存储学生信息:key=班级号,value=学生姓名+成绩(用pair封装)
using StuMap = multimap<int, pair<string, int>>;

// 添加学生成绩
void addStudent(StuMap& mm, int classId, const string& name, int score) {
    mm.emplace(classId, make_pair(name, score));
}

// 查询某个班级的所有学生成绩
void queryClassScore(const StuMap& mm, int classId) {
    auto range = mm.equal_range(classId);
    if (range.first == range.second) {
        cout << "班级" << classId << "无学生信息" << endl;
        return;
    }
    cout << "\n班级" << classId << "学生成绩:" << endl;
    for (auto it = range.first; it != range.second; ++it) {
        cout << "姓名:" << it->second.first << ",成绩:" << it->second.second << endl;
    }
}

int main() {
    StuMap mm;
    // 添加学生数据
    addStudent(mm, 1, "张三", 95);
    addStudent(mm, 1, "李四", 88);
    addStudent(mm, 2, "王五", 92);
    addStudent(mm, 2, "赵六", 79);
    addStudent(mm, 1, "孙七", 90);
    
    // 查询班级1成绩
    queryClassScore(mm, 1);
    // 查询班级2成绩
    queryClassScore(mm, 2);
    // 查询不存在的班级3
    queryClassScore(mm, 3);
    
    return 0;
}

输出结果:

text 复制代码
班级1学生成绩:
姓名:张三,成绩:95
姓名:李四,成绩:88
姓名:孙七,成绩:90

班级2学生成绩:
姓名:王五,成绩:92
姓名:赵六,成绩:79

班级3无学生信息

3.2 场景2:单词索引(按单词出现的行号映射)

需求:读取一段文本,记录每个单词出现的所有行号,支持查询某个单词的所有出现位置。

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

// 单词索引:key=单词,value=出现的行号
using WordIndex = multimap<string, int>;

// 构建单词索引(忽略大小写,简单处理标点)
void buildWordIndex(WordIndex& mm, const string& text) {
    stringstream ss(text);
    string line;
    int lineNum = 0;
    
    // 按行读取文本
    while (getline(ss, line)) {
        lineNum++;
        stringstream lineSs(line);
        string word;
        
        // 按空格分割单词
        while (lineSs >> word) {
            // 简单处理:移除单词末尾的标点(逗号、句号、分号)
            if (!word.empty() && ispunct(word.back())) {
                word.pop_back();
            }
            // 转换为小写(统一索引)
            for (char& c : word) {
                c = tolower(c);
            }
            // 插入索引
            mm.emplace(word, lineNum);
        }
    }
}

// 查询单词出现的所有行号
void queryWord(const WordIndex& mm, const string& word) {
    string lowerWord = word;
    for (char& c : lowerWord) {
        c = tolower(c);
    }
    
    auto range = mm.equal_range(lowerWord);
    if (range.first == range.second) {
        cout << "单词\"" << word << "\"未出现" << endl;
        return;
    }
    
    cout << "单词\"" << word << "\"出现的行号:";
    for (auto it = range.first; it != range.second; ++it) {
        cout << it->second << " ";
    }
    cout << endl;
}

int main() {
    // 测试文本
    string text = R"(Hello world!
Hello C++.
World is beautiful.
C++ is fun.)";
    
    WordIndex mm;
    buildWordIndex(mm, text);
    
    // 查询单词
    queryWord(mm, "hello");
    queryWord(mm, "C++");
    queryWord(mm, "world");
    queryWord(mm, "java");
    
    return 0;
}

输出结果:

text 复制代码
单词"hello"出现的行号:1 2 
单词"C++"出现的行号:2 4 
单词"world"出现的行号:1 3 
单词"java"未出现

四、multimap 使用注意事项

  1. 禁止修改key值:multimap的key是排序的依据,修改key会破坏红黑树的有序性,导致容器行为异常。若需修改key,需先删除原键值对,再插入新的键值对。

  2. 不支持operator[]:因key可重复,无法通过key唯一定位元素,因此multimap没有实现operator[]接口,需通过find()、equal_range()等接口访问元素。

  3. 自定义排序规则:默认按key升序排列,可通过自定义比较函数修改排序(如降序)。自定义比较函数需满足"严格弱序"(即不可传递的等价关系)。

  4. 迭代器失效问题:插入元素时,红黑树可能会旋转调整,但迭代器不会失效(除了指向被删除元素的迭代器);删除元素时,只有指向被删除元素的迭代器失效,其他迭代器仍有效。

  5. 效率考量:若无需key重复,优先使用map(map的查找、插入效率略高于multimap,因无需处理重复key);若需要频繁按key分组查询,multimap是最优选择。

  6. 线程安全性:与所有STL容器一致,multimap不保证线程安全,多线程环境下并发读写需手动加锁(如使用std::mutex)。

五、总结

multimap是C++ STL中用于处理"一对多"映射关系的有序容器,底层基于红黑树实现,确保了元素有序性和高效的插入、删除、查找操作。其核心优势在于支持key重复,通过equal_range()等接口可便捷地获取同一key对应的所有value,特别适合分组、索引等场景。

使用时需注意:禁止修改key值、不支持operator[]接口,若无需key重复应优先选择map。掌握multimap的核心接口(尤其是equal_range())和适用场景,能帮助我们在处理多值映射问题时写出更简洁、高效的代码。

相关推荐
好奇龙猫2 小时前
【大学院-筆記試験練習:线性代数和数据结构(6)】
学习
superman超哥2 小时前
Rust 异步并发基石:异步锁(Mutex、RwLock)的设计与深度实践
开发语言·后端·rust·编程语言·rust异步并发·rust异步锁·rust mutex
码农水水2 小时前
阿里Java面试被问:RocketMQ的消息轨迹追踪实现
java·开发语言·windows·算法·面试·rocketmq·java-rocketmq
●VON2 小时前
使用 OpenAgents 搭建基于智谱 GLM 的本地智能体(Agent)
学习·安全·制造·智能体·von
丝斯20112 小时前
AI学习笔记整理(45)——大模型数据读取技术与模型部署
人工智能·笔记·学习
APIshop2 小时前
实战解析电商api:1688item_search-按关键字搜索商品数据
开发语言·python
向上的车轮2 小时前
Zed 项目GPUI :用 Rust + GPU 渲染的现代化 UI 框架
开发语言·ui·rust
APIshop2 小时前
实战解析:1688详情api商品sku、主图数据
java·服务器·windows