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

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

在C++标准模板库(STL)的关联容器中,multiset是一种支持元素重复存储的有序集合。它与基础的set容器核心逻辑一致,均基于红黑树(自平衡二叉搜索树)实现,保证了元素的有序性和高效的增删查操作;但区别于set的"元素唯一性"限制,multiset允许相同值的元素共存,这使其在处理需要存储重复数据且需有序排列的场景时极具优势。本文将从底层原理出发,详细拆解multiset的核心特性、常用接口,结合实战案例演示具体用法,并对比set明确适用边界,帮助大家彻底掌握这一实用容器。

一、multiset 核心原理与特性

要理解multiset的行为逻辑,首先需要明确其底层实现和核心特性,这是正确使用它的基础。

1.1 底层实现:红黑树的支撑

multiset与set、map、multimap同属STL关联容器,底层均依赖红黑树(一种自平衡的二叉搜索树)实现。红黑树通过节点颜色(红/黑)的约束和动态旋转操作,确保树的高度始终维持在O(log n)级别,从而保证了插入、删除、查找等核心操作的时间复杂度稳定为O(log n)。

对于multiset而言,红黑树的"二叉搜索树特性"(左子树节点值 ≤ 父节点值 ≤ 右子树节点值,支持重复元素)是其"有序性"和"重复存储"的核心保障。当插入新元素时,红黑树会根据元素值找到合适的插入位置,维持整体有序;当删除元素时,会通过旋转调整保持树的平衡,确保后续操作效率。

1.2 核心特性总结

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

  • 允许重复:支持存储多个值相同的元素,无"唯一性"限制;

  • 高效操作:插入、删除、查找的时间复杂度均为O(log n),效率稳定;

  • 不可直接修改元素:元素是排序的依据,直接修改会破坏红黑树的有序性,需先删除旧元素再插入新元素;

  • 迭代器特性:支持双向迭代器(可++/--遍历),不支持随机访问;插入/删除元素时,除指向被删除元素的迭代器外,其他迭代器均不失效;

  • 无下标访问:因元素有序但无索引概念,不支持operator[]和at()接口。

1.3 与set的核心区别

multiset与set的底层实现完全一致,核心差异仅在于对"元素唯一性"的要求,这也导致了两者接口和用法的细微区别。通过下表可快速区分:

特性/接口 set multiset
元素唯一性 不允许重复元素,插入重复元素会失败 允许重复元素,插入重复元素始终成功
insert()返回值 返回pair<iterator, bool>,bool标记插入是否成功 仅返回插入位置的迭代器(插入必成功)
查找与删除 find()返回唯一匹配元素的迭代器;erase(val)删除唯一匹配元素 find()返回第一个匹配元素的迭代器;erase(val)删除所有匹配元素
核心适用场景 存储不重复的有序数据(如去重、判重) 存储可重复的有序数据(如统计频率、保留重复序列)

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

使用multiset前,需包含头文件 <set>(与set共用头文件),并使用std命名空间(或显式指定std::multiset)。multiset的接口与set大部分一致,以下重点讲解核心接口及多元素场景下的特殊用法。

2.1 构造与析构

接口原型 功能说明 示例
multiset(); 默认构造,创建空multiset(默认升序) std::multiset ms;
multiset(const Compare& comp); 自定义比较函数构造空multiset(如降序) std::multiset<int, std::greater> ms_desc;
multiset(InputIterator first, InputIterator last); 迭代器构造,拷贝[first, last)区间的元素 std::vector vec{1,2,2,3}; std::multiset ms(vec.begin(), vec.end());
multiset(const multiset& other); 拷贝构造函数 std::multiset ms2(ms);
~multiset(); 析构函数,释放红黑树所有节点资源 -

2.2 迭代器相关

multiset的迭代器用于遍历有序元素,核心接口与其他关联容器一致:

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

int main() {
    // 构造multiset,包含重复元素,默认升序
    multiset<int> ms = {2, 1, 3, 2, 4, 1};
    
    // 正向遍历(升序)
    cout << "正向遍历(升序):";
    for (auto it = ms.begin(); it != ms.end(); ++it) {
        cout << *it << " "; // 输出:1 1 2 2 3 4
    }
    cout << endl;
    
    // 反向遍历(降序)
    cout << "反向遍历(降序):";
    for (auto it = ms.rbegin(); it != ms.rend(); ++it) {
        cout << *it << " "; // 输出:4 3 2 2 1 1
    }
    cout << endl;
    
    // 自定义降序的multiset
    multiset<int, greater<int>> ms_desc = {2, 1, 3, 2, 4, 1};
    cout << "自定义降序遍历:";
    for (auto x : ms_desc) {
        cout << x << " "; // 输出:4 3 2 2 1 1
    }
    cout << endl;
    
    return 0;
}

2.3 容量相关

接口 功能说明
size() 返回当前元素的个数(包含重复元素)
empty() 判断容器是否为空(空返回true,否则返回false)
max_size() 返回容器可容纳的最大元素个数(受系统内存限制)

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

multiset的插入操作因支持重复元素,接口返回值与set不同;删除操作可按元素值、迭代器删除,灵活性较高。

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

int main() {
    multiset<int> ms;
    
    // 1. 插入单个元素
    ms.insert(2);
    ms.insert(2); // 插入重复元素,成功
    ms.emplace(1); // emplace直接构造,效率更高
    cout << "插入后size:" << ms.size() << endl; // 输出:3
    cout << "插入后元素:";
    for (auto x : ms) cout << x << " "; // 输出:1 2 2
    cout << endl;
    
    // 2. 插入区间元素
    int arr[] = {3, 2, 4};
    ms.insert(arr, arr + 3);
    cout << "插入区间后元素:";
    for (auto x : ms) cout << x << " "; // 输出:1 2 2 2 3 4
    cout << endl;
    
    // 3. 按元素值删除(删除所有2)
    size_t delCount = ms.erase(2);
    cout << "删除元素2的个数:" << delCount << endl; // 输出:3
    cout << "删除后元素:";
    for (auto x : ms) cout << x << " "; // 输出:1 3 4
    cout << endl;
    
    // 4. 按迭代器删除(删除第一个元素)
    auto it = ms.begin();
    ms.erase(it);
    cout << "按迭代器删除后元素:";
    for (auto x : ms) cout << x << " "; // 输出:3 4
    cout << endl;
    
    // 5. 清空容器
    ms.clear();
    cout << "clear后empty:" << ms.empty() << endl; // 输出:1(true)
    
    return 0;
}

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

因multiset支持重复元素,查找操作除了常规的find(),还提供了专门用于定位"重复元素区间"的接口(lower_bound()、upper_bound()、equal_range()),这是处理重复元素的核心关键。

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

int main() {
    multiset<int> ms = {1, 2, 2, 3, 2, 4, 1};
    
    // 1. 查找第一个值为2的元素
    auto findIt = ms.find(2);
    if (findIt != ms.end()) {
        cout << "第一个值为2的元素:" << *findIt << endl; // 输出:2
    }
    
    // 2. 统计值为2的元素个数
    size_t count = ms.count(2);
    cout << "值为2的元素个数:" << count << endl; // 输出:3
    
    // 3. 用lower_bound和upper_bound遍历所有值为2的元素
    auto lowIt = ms.lower_bound(2);
    auto upIt = ms.upper_bound(2);
    cout << "lower_bound & upper_bound遍历值为2的元素:";
    for (auto it = lowIt; it != upIt; ++it) {
        cout << *it << " "; // 输出:2 2 2
    }
    cout << endl;
    
    // 4. 用equal_range遍历所有值为2的元素(更简洁)
    auto range = ms.equal_range(2);
    cout << "equal_range遍历值为2的元素:";
    for (auto it = range.first; it != range.second; ++it) {
        cout << *it << " "; // 输出:2 2 2
    }
    cout << endl;
    
    // 5. 查找不存在的元素
    auto noIt = ms.find(5);
    if (noIt == ms.end()) {
        cout << "值为5的元素不存在" << endl;
    }
    
    return 0;
}

三、multiset 实战案例

multiset的核心优势是"有序存储+支持重复",以下通过两个经典场景演示其实际应用:

3.1 场景1:统计数组元素出现频率并排序

需求:给定一个整数数组,统计每个元素的出现次数,并按元素值升序输出结果。

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

void countAndSortElements(const vector<int>& arr) {
    // 1. 插入数组元素到multiset(自动排序+保留重复)
    multiset<int> ms(arr.begin(), arr.end());
    
    // 2. 遍历统计每个元素的出现次数
    auto it = ms.begin();
    while (it != ms.end()) {
        int val = *it;
        // 统计当前元素的个数
        size_t count = ms.count(val);
        // 输出结果
        cout << "元素 " << val << " 出现次数:" << count << endl;
        // 跳过当前元素的所有重复项
        it = ms.upper_bound(val);
    }
}

int main() {
    vector<int> arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
    cout << "数组元素频率统计(按元素升序):" << endl;
    countAndSortElements(arr);
    return 0;
}

输出结果:

text 复制代码
数组元素频率统计(按元素升序):
元素 1 出现次数:2
元素 2 出现次数:1
元素 3 出现次数:2
元素 4 出现次数:1
元素 5 出现次数:3
元素 6 出现次数:1
元素 9 出现次数:1

3.2 场景2:维护一个有序的数据流,支持快速查询中位数

需求:设计一个数据结构,支持向数据流中插入整数,并能快速查询当前数据流的中位数。中位数是有序列表中间的数(若列表长度为偶数,取中间两个数的平均值)。

思路:利用两个multiset构建"双堆"结构------左半部分(大根堆)存储较小的元素,右半部分(小根堆)存储较大的元素,确保左半部分元素个数≥右半部分(最多相差1)。此时:

  • 若元素总数为奇数,左半部分的最大值(大根堆顶)即为中位数;

  • 若元素总数为偶数,中位数为左半部分最大值与右半部分最小值的平均值。

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

class MedianFinder {
private:
    // 左半部分:大根堆(用greater排序,最大元素在末尾)
    multiset<int, greater<int>> left;
    // 右半部分:小根堆(默认升序,最小元素在开头)
    multiset<int> right;
public:
    // 插入元素
    void addNum(int num) {
        // 先插入左半部分,再调整平衡
        left.insert(num);
        
        // 确保左半部分的最大元素 ≤ 右半部分的最小元素
        if (!left.empty() && !right.empty() && *left.begin() > *right.begin()) {
            int val = *left.begin();
            left.erase(left.begin());
            right.insert(val);
        }
        
        // 确保左半部分元素个数最多比右半部分多1
        if (left.size() > right.size() + 1) {
            int val = *left.begin();
            left.erase(left.begin());
            right.insert(val);
        }
        // 确保右半部分元素个数不超过左半部分
        if (right.size() > left.size()) {
            int val = *right.begin();
            right.erase(right.begin());
            left.insert(val);
        }
    }
    
    // 查询中位数
    double findMedian() {
        if (left.size() > right.size()) {
            // 奇数个元素,左半部分最大值为中位数
            return *left.begin();
        } else {
            // 偶数个元素,取两部分顶元素的平均值
            return (*left.begin() + *right.begin()) / 2.0;
        }
    }
};

int main() {
    MedianFinder mf;
    mf.addNum(1);
    mf.addNum(2);
    cout << "当前中位数:" << mf.findMedian() << endl; // 输出:1.5
    mf.addNum(3);
    cout << "当前中位数:" << mf.findMedian() << endl; // 输出:2.0
    mf.addNum(4);
    cout << "当前中位数:" << mf.findMedian() << endl; // 输出:2.5
    return 0;
}

四、multiset 使用注意事项

  1. 禁止直接修改元素:multiset的元素是红黑树排序的依据,直接修改元素值会破坏树的有序性,导致容器行为异常。若需修改元素,必须先删除旧元素(erase()),再插入新元素(insert())。

  2. 自定义比较函数的规则:自定义排序时,比较函数必须满足"严格弱序"(即不可传递的等价关系)。例如,自定义降序使用std::greater,若自定义函数,需确保对于任意a、b,!comp(a,b) && !comp(b,a) 表示a和b等价(可共存)。

  3. 迭代器失效问题:插入元素时,红黑树的旋转调整不会导致迭代器失效;删除元素时,只有指向被删除元素的迭代器失效,其他迭代器(包括指向其他元素的迭代器、begin()、end()等)均有效。

  4. 效率对比与选择

    • 若需存储不重复的有序数据,优先使用set(set的查找、插入效率略高于multiset,因无需处理重复元素的冗余逻辑);

    • 若需存储重复的有序数据,优先使用multiset;若无需有序,可使用unordered_multiset(哈希表实现,平均插入/查找效率O(1),但无序)。

  5. 处理重复元素的最佳实践:遍历某一重复元素的所有实例时,优先使用equal_range()接口,其效率高于"find() + 循环计数",且代码更简洁。

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

五、总结

multiset是C++ STL中用于存储"有序重复元素"的关联容器,底层基于红黑树实现,兼具有序性和高效操作的特性。其核心优势在于支持重复元素的有序存储,通过equal_range()等接口可便捷地处理重复元素的遍历与统计,特别适合频率统计、有序数据流维护等场景。

使用时需注意:禁止直接修改元素、不支持下标访问,若无需重复元素应优先选择set。掌握multiset的核心接口(尤其是equal_range())和适用场景,能帮助我们在处理有序重复数据时写出更高效、简洁的代码。

相关推荐
求梦8202 小时前
JVM学习
jvm·学习
am心2 小时前
学习笔记-菜品接口-菜品分页查询
笔记·学习
小猪咪piggy2 小时前
【leetcode100】回溯
数据结构·算法
一眼万里*e2 小时前
MavLink消息协议
c++
丝斯20112 小时前
AI学习笔记整理(44)——大规模预训练模型数据处理管道Pipeline
人工智能·笔记·学习
知识分享小能手3 小时前
Oracle 19c入门学习教程,从入门到精通, —— 知识点详解(1)
数据库·学习·oracle
lsx2024063 小时前
Eclipse 添加书签
开发语言
易营宝3 小时前
高效的跨境电商广告优化系统:易营宝广告投放实操指南
大数据·开发语言·人工智能·php
superman超哥3 小时前
路由的艺术:Rust Web 框架中的高效匹配与类型安全提取
开发语言·rust·编程语言·rust web框架·rust路由