news 2026/7/29 5:20:51

C++ STL multiset 容器详解:红黑树实现、核心操作与实战应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ STL multiset 容器详解:红黑树实现、核心操作与实战应用

1. 项目概述:为什么你需要深入了解Multiset?

如果你在C++项目中处理过需要频繁插入、删除,同时又需要快速查找、并且允许元素重复的集合数据,那么你很可能已经接触过或者听说过std::multiset。它不像std::vector那样强调线性顺序和随机访问,也不像std::unordered_set那样追求极致的O(1)平均查找时间。multiset的定位非常独特:它是一个基于红黑树实现的有序关联容器,核心价值在于自动排序允许重复键。这意味着,你无需在每次插入后手动调用std::sort,容器内部始终维持着元素的有序状态;同时,你可以存放多个完全相同的值,这在统计频率、管理具有相同优先级的多任务等场景下至关重要。

很多初学者容易将multisetsetmultimap混淆。简单来说,set是元素唯一且有序的集合;multimap是允许键重复的键值对有序集合;而multiset更像是set的“宽容版”,放弃了元素的唯一性约束。与unordered_multiset相比,虽然后者在平均情况下插入和查找更快(哈希表实现),但它不保证元素顺序,迭代顺序是未定义的。因此,选择multiset还是unordered_multiset,本质上是在“需要元素有序”和“追求极致性能”之间做权衡。

在实际开发中,multiset的应用场景比你想象的更广泛。例如,在游戏开发中管理同一优先级下的多个游戏对象;在金融分析中维护一个实时更新的、允许重复的价格列表;在日志系统中存储带时间戳的日志条目(时间戳可能相同);甚至是在算法竞赛中,快速实现一些需要动态维护有序序列且支持重复元素的题目。理解multiset,不仅是掌握一个容器,更是理解一种基于平衡二叉搜索树的数据管理思想。接下来,我将从设计思路、核心操作到实战避坑,为你彻底拆解这个强大而优雅的容器。

2. 核心设计思路与内部机制剖析

2.1 底层数据结构:红黑树如何支撑Multiset的特性?

std::multiset的几乎所有行为都源于其底层实现——红黑树。红黑树是一种自平衡的二叉搜索树,它通过在节点中增加一个颜色标记(红或黑)和一套复杂的旋转、变色规则,来确保树的高度大致平衡。这种平衡性保证了对于multiset的核心操作(插入、删除、查找)的时间复杂度都能稳定在O(log n),这里n是容器中元素的数量。这是multiset性能的基石。

为什么是红黑树而不是其他平衡树(如AVL树)?这涉及到STL设计时的权衡。AVL树是更严格的平衡树,其查找性能略优于红黑树,但为了维持更严格的平衡,在插入和删除时需要更频繁的旋转操作。红黑树放宽了平衡条件,它只要求从根到叶子的任何路径上,不能有两个连续的红色节点,并且从根到所有叶子节点的路径上,黑色节点的数量相同。这种“近似平衡”减少了插入和删除时的旋转次数,使得红黑树在整体性能上,尤其是对于修改操作频繁的场景,综合表现更优。STL选择红黑树,正是看重其在频繁增删场景下依然能保持良好性能的平衡能力。

对于multiset允许重复元素的特性,红黑树是如何处理的呢?它并没有采用在每个树节点存储一个链表来挂载重复值的方式(那会退化成哈希桶的思想)。相反,红黑树的标准实现中,对于键值比较相等的节点(即!comp(a,b) && !comp(b,a)),它允许它们同时存在于树中。在插入时,如果遇到键值相等的节点,根据具体的实现策略(通常是插入到相等范围的任意位置,但必须保持树的有序性),新节点会被安放进去。当你使用equal_rangecount函数时,算法会利用二叉搜索树的有序性,高效地定位到所有相等元素的区间。

注意:由于底层是红黑树,multiset中的元素必须是可比较的。这意味着元素类型需要支持<运算符,或者你在构造multiset时传入一个自定义的比较函数对象。这个比较关系定义了树的“序”,至关重要。

2.2 关键特性与性能权衡

理解了底层是红黑树,我们就能系统地把握multiset的关键特性:

  1. 自动排序:元素在插入时即被放置在正确的位置,整个容器始终有序。这是通过红黑树的插入算法保证的。
  2. 允许重复:这是与set最根本的区别。比较函数(默认为std::less)定义“小于”关系,当两个元素ab互不小于对方时(即a不小于bb不小于a),它们被视为“等价”而非“相等”。multiset允许存在多个彼此“等价”的元素。
  3. 稳定的对数时间复杂度:插入(insert)、查找(find)、删除(erase一个迭代器)单个元素都是O(log n)。遍历整个容器是O(n)。这里“稳定”指的是最坏情况下的复杂度,得益于红黑树的自平衡。
  4. 双向迭代器:你可以使用++--操作符向前或向后遍历元素。由于是有序序列,正向迭代器(begin()end())遍历得到的是升序序列。迭代器在元素被删除后(除了被删除的那个元素的迭代器)通常不会失效,除非发生了树的重新平衡导致节点内存地址变化,但STL的实现保证了除指向被删除元素的迭代器外,其他迭代器、引用和指针的稳定性。
  5. 空间开销:每个元素存储在一个树节点中,节点除了存储元素值,还需要存储左右子节点指针、父节点指针以及颜色标记。因此,它的内存开销比vectorarray这类连续存储容器要大。

std::priority_queue的对比常让人困惑。两者都提供了一种“有序”的视图。主要区别在于:

  • 访问权限priority_queue(优先队列)只允许访问顶部的最大(或最小)元素(top()),而multiset允许你访问、迭代任何一个元素。
  • 底层结构priority_queue默认底层是vector配合堆算法,multiset是红黑树。
  • 用途priority_queue适用于需要不断处理当前最高/最低优先级任务的场景(如任务调度)。multiset适用于需要维护一个完全有序的、可随机访问(通过查找)和遍历的集合的场景。
  • 重复元素:两者都支持重复元素。

选择哪一个,取决于你的需求是“只需要处理当前极值”还是“需要操作整个有序集合”。

3. 核心操作详解与实战代码示例

理论说得再多,不如一行代码。让我们进入实战环节,我会结合具体场景,展示multiset最常用和最关键的操作。

3.1 容器的创建与初始化

创建multiset非常灵活。最常用的是使用默认构造函数和范围构造函数。

#include <iostream> #include <set> // multiset 和 set 都在这个头文件 #include <vector> int main() { // 1. 默认构造:空multiset,使用默认的std::less<T>比较 std::multiset<int> ms1; // 2. 使用初始化列表构造 std::multiset<int> ms2 = {5, 2, 8, 2, 5, 1}; // 注意:允许重复 // 此时ms2内容为:1, 2, 2, 5, 5, 8 (已排序) // 3. 使用迭代器范围构造 std::vector<int> vec = {9, 3, 3, 6, 1}; std::multiset<int> ms3(vec.begin(), vec.end()); // ms3内容:1, 3, 3, 6, 9 // 4. 自定义比较函数:实现降序排列 // 使用函数对象(仿函数) struct Greater { bool operator()(int a, int b) const { return a > b; // 降序 } }; std::multiset<int, Greater> ms4 = {5, 2, 8, 2}; // ms4内容:8, 5, 2, 2 (降序) // 或者使用lambda表达式(C++11及以上) auto cmp = [](int a, int b) { return a > b; }; std::multiset<int, decltype(cmp)> ms5(cmp); ms5.insert({5, 2, 8, 2}); // ms5内容:8, 5, 2, 2 // 5. 拷贝构造和赋值 std::multiset<int> ms6(ms2); // 拷贝构造 std::multiset<int> ms7 = ms3; // 拷贝赋值 return 0; }

提示:自定义比较函数时,必须确保它满足严格弱序关系。简单说,它需要像<运算符一样,不可自反(comp(a, a)false)、可传递(如果comp(a, b)comp(b, c)为真,则comp(a, c)为真)、可比较(任意两个元素,comp(a,b)comp(b,a)至少有一个为真,除非它们“等价”)。使用std::greater<>是一个常见的降序方案。

3.2 元素的插入、查找与删除

这是multiset最核心的三个操作。

插入 (insert)

std::multiset<std::string> taskSet; // 1. 插入单个值,返回指向新元素的迭代器 auto it = taskSet.insert("Debug"); // it 指向新插入的"Debug" // 2. 插入单个值(带提示位置)。提示位置hint是一个迭代器, // 表示搜索起点,可能提高插入效率(如果提示准确)。 // 返回指向新元素的迭代器。 auto hint = taskSet.find("Debug"); taskSet.insert(hint, "Test"); // 在hint附近开始搜索插入位置 // 3. 插入一个初始化列表 taskSet.insert({"Build", "Commit", "Test", "Build"}); // 容器现在包含:Build, Build, Commit, Debug, Test, Test (按字母序) // 4. 使用迭代器范围插入 std::vector<std::string> moreTasks = {"Review", "Deploy"}; taskSet.insert(moreTasks.begin(), moreTasks.end());

查找 (find,count,equal_range,lower_bound/upper_bound)由于允许重复,查找操作比set更丰富。

std::multiset<int> scores = {85, 90, 78, 90, 92, 85, 88}; // 1. find: 返回指向第一个找到的等价元素的迭代器,未找到则返回end() auto it = scores.find(90); if (it != scores.end()) { std::cout << "Found: " << *it << std::endl; // 输出 Found: 90 // 注意:如果有多个90,find只返回第一个 } // 2. count: 返回等价元素的数量 size_t c = scores.count(85); std::cout << "Number of 85: " << c << std::endl; // 输出 Number of 85: 2 // 3. equal_range: 返回一个pair<iterator, iterator>, // 表示等价元素的范围 [first, last)。这是处理重复键最强大的工具。 auto range = scores.equal_range(90); std::cout << "All 90s: "; for (auto it = range.first; it != range.second; ++it) { std::cout << *it << " "; } std::cout << std::endl; // 输出 All 90s: 90 90 // 4. lower_bound / upper_bound: // lower_bound(k): 返回第一个不小于k的元素的迭代器(即第一个>=k的) // upper_bound(k): 返回第一个大于k的元素的迭代器 // 两者结合也可以用来获取范围,等同于equal_range auto low = scores.lower_bound(85); // 指向第一个85 auto up = scores.upper_bound(85); // 指向第一个大于85的元素(88) std::cout << "Range of 85: "; for (auto it = low; it != up; ++it) { std::cout << *it << " "; } std::cout << std::endl; // 输出 Range of 85: 85 85

删除 (erase)删除操作需要小心处理迭代器失效问题。

std::multiset<int> data = {1, 3, 3, 3, 5, 7}; // 1. 通过迭代器删除单个元素。最安全,时间复杂度O(1) 或 O(log n)。 auto it = data.find(5); if (it != data.end()) { data.erase(it); // 删除找到的那个5 } // data: 1, 3, 3, 3, 7 // 2. 通过值删除。删除所有等价于该值的元素,返回被删除的元素个数。 size_t num_removed = data.erase(3); // 删除所有的3 std::cout << "Removed " << num_removed << " elements." << std::endl; // 输出 Removed 3 elements. // data: 1, 7 // 3. 通过迭代器范围删除。删除[first, last)区间内的元素。 auto first = data.lower_bound(5); auto last = data.upper_bound(10); data.erase(first, last); // 删除所有 >=5 且 <=10 的元素?注意:upper_bound(10)可能指向end() // 更安全的做法是确保范围有效。 // data: 1 // 重要:删除元素后,指向被删除元素的迭代器、引用和指针会失效。 // 但其他元素的迭代器通常保持有效(得益于红黑树的实现)。

3.3 迭代与容量查询

遍历multiset很简单,因为它提供了双向迭代器。

std::multiset<char> letters = {'b', 'a', 'c', 'a', 'd'}; // 1. 正向迭代 (升序) std::cout << "Ascending: "; for (auto it = letters.begin(); it != letters.end(); ++it) { std::cout << *it << " "; } // 输出:Ascending: a a b c d // 2. 反向迭代 (降序) std::cout << "\nDescending: "; for (auto rit = letters.rbegin(); rit != letters.rend(); ++rit) { std::cout << *rit << " "; } // 输出:Descending: d c b a a // 3. 基于范围的for循环 (C++11) std::cout << "\nRange-for: "; for (const auto& ch : letters) { std::cout << ch << " "; } // 输出:Range-for: a a b c d // 容量查询 std::cout << "\nSize: " << letters.size() << std::endl; // 元素个数: 5 std::cout << "Empty? " << std::boolalpha << letters.empty() << std::endl; // 是否为空: false // multiset 没有 capacity() 成员函数,因为其内存不是预分配的。

4. 高级用法与性能优化技巧

掌握了基本操作,我们来看看如何更高效、更安全地使用multiset

4.1 利用Hint提升插入性能

insert函数有一个重载版本,接受一个迭代器作为“提示”。如果这个提示位置恰好是新元素的正确插入位置(或者非常接近),那么插入操作可以从O(log n)优化到分摊常数时间

std::multiset<int> largeSet; // 假设我们正在按顺序插入一个已经大致有序的巨大数据集 std::vector<int> sortedData = {/* ... 大量数据,基本有序 ... */}; auto hint = largeSet.end(); // 初始提示设为end() for (int value : sortedData) { // 因为数据大致有序,新值很可能插入在现有集合的末尾附近。 // 使用hint作为插入起点。 hint = largeSet.insert(hint, value); // insert返回新插入元素的位置,作为下一次插入的hint。 }

这个技巧在批量插入有序或近乎有序的数据时非常有效。但如果提示位置离正确位置很远,性能可能反而比普通插入更差,因为搜索需要从提示位置开始并可能回溯。

4.2 自定义比较函数与复杂对象存储

multiset的强大之处在于它能存储任何定义了严格弱序的类型。对于自定义类或结构体,你需要提供比较方法。

#include <string> struct Player { std::string name; int score; int level; // 按score降序排列,如果score相同则按level升序排列 bool operator<(const Player& other) const { if (score != other.score) { return score > other.score; // 分数高的在前(降序) } return level < other.level; // 分数相同时,等级低的在前(升序) } }; int main() { // 使用默认的 operator< 进行比较 std::multiset<Player> leaderboard; leaderboard.insert({"Alice", 100, 10}); leaderboard.insert({"Bob", 150, 5}); leaderboard.insert({"Charlie", 100, 8}); // 与Alice分数相同,但level不同 std::cout << "Leaderboard:\n"; for (const auto& p : leaderboard) { std::cout << p.name << " - Score: " << p.score << ", Level: " << p.level << std::endl; } // 输出: // Bob - Score: 150, Level: 5 // Alice - Score: 100, Level: 10 // Charlie - Score: 100, Level: 8 // 注意:Bob分数最高排第一。Alice和Charlie分数相同,但Charlie等级(8) < Alice等级(10),所以Charlie排在Alice前面。 return 0; }

你也可以使用独立的函数对象,这在不想修改类定义或者需要多种排序方式时非常有用。

struct Player { std::string name; int score; }; // 自定义比较仿函数:只按分数升序排 struct CompareByScore { bool operator()(const Player& a, const Player& b) const { return a.score < b.score; } }; int main() { std::multiset<Player, CompareByScore> leaderboard; leaderboard.insert({"Alice", 100}); leaderboard.insert({"Bob", 150}); leaderboard.insert({"Charlie", 100}); // 允许重复分数 for (const auto& p : leaderboard) { std::cout << p.name << ": " << p.score << std::endl; } // 输出:Alice: 100, Charlie: 100, Bob: 150 return 0; }

4.3 与其它容器的交互与数据迁移

你经常需要将multiset的数据与其他容器进行转换。

// 1. multiset 转 vector (例如需要随机访问或调用vector特有算法时) std::multiset<int> ms = {5, 1, 4, 4, 2}; std::vector<int> vec(ms.begin(), ms.end()); // 利用迭代器范围构造 // vec: 1, 2, 4, 4, 5 // 2. vector 转 multiset (去重?不,multiset保留所有重复) std::vector<int> input = {7, 3, 7, 1}; std::multiset<int> ms2(input.begin(), input.end()); // ms2: 1, 3, 7, 7 // 3. 获取唯一元素集合(相当于set的功能) // 方法一:使用std::set直接构造 std::set<int> uniqueSet(ms.begin(), ms.end()); // 方法二:利用multiset有序的特性,手动遍历去重(更高效,无需额外比较) std::vector<int> uniqueVec; if (!ms.empty()) { auto prev = ms.begin(); uniqueVec.push_back(*prev); for (auto it = std::next(ms.begin()); it != ms.end(); ++it) { if (*it != *prev) { uniqueVec.push_back(*it); } prev = it; } }

5. 实战场景深度剖析与避坑指南

理论结合实战,才能真正掌握。下面我们通过几个典型场景,并总结常见的“坑”。

5.1 场景一:实时排行榜维护

假设你正在开发一个游戏,需要维护一个实时更新的玩家分数排行榜,允许并列。

#include <iostream> #include <set> #include <string> #include <chrono> #include <random> struct GamePlayer { std::string uid; int score; // 按分数降序排列,分数相同按uid升序(确保唯一序) bool operator<(const GamePlayer& other) const { if (score != other.score) { return score > other.score; // 降序 } return uid < other.uid; // 打破平局 } }; class Leaderboard { private: std::multiset<GamePlayer> players; public: void addOrUpdatePlayer(const std::string& uid, int newScore) { // 先查找该玩家是否已在榜上(根据uid查找) // 注意:我们的排序规则是score和uid,无法直接用uid查找。 // 一种方法是线性查找(O(n)),对于排行榜,玩家数量通常可控。 // 更高效的方法是维护一个额外的std::unordered_map<std::string, iterator>来快速定位。 // 这里为了演示简单,使用线性查找。 auto it = std::find_if(players.begin(), players.end(), [&uid](const GamePlayer& p) { return p.uid == uid; }); if (it != players.end()) { // 找到,先删除旧记录 players.erase(it); } // 插入新记录 players.insert({uid, newScore}); } void printTopN(int n) const { std::cout << "--- Top " << n << " Players ---\n"; int rank = 1; int lastScore = -1; int skipRank = 0; for (auto it = players.begin(); it != players.end() && rank <= n + skipRank; ++it) { // 处理并列排名 if (it->score != lastScore) { lastScore = it->score; if (rank > n) break; // 已经打印完前N名(考虑并列) std::cout << "Rank " << rank << ": " << it->uid << " - " << it->score << std::endl; rank++; } else { // 分数相同,并列排名 std::cout << "Rank " << rank-1 << ": " << it->uid << " - " << it->score << " (tied)" << std::endl; skipRank++; // 因为并列多打印了一个,需要调整循环条件 } } } const GamePlayer* getPlayerAtRank(int rank) const { if (rank < 1 || rank > players.size()) return nullptr; auto it = players.begin(); std::advance(it, rank - 1); // 注意:advance是O(n)操作,谨慎使用! return &(*it); } }; int main() { Leaderboard lb; lb.addOrUpdatePlayer("player_001", 2500); lb.addOrUpdatePlayer("player_002", 1800); lb.addOrUpdatePlayer("player_003", 3100); lb.addOrUpdatePlayer("player_004", 1800); // 与player_002并列 lb.addOrUpdatePlayer("player_002", 2000); // 更新player_002分数 lb.printTopN(5); // 输出: // --- Top 5 Players --- // Rank 1: player_003 - 3100 // Rank 2: player_002 - 2000 // Rank 3: player_001 - 2500 // Rank 4: player_004 - 1800 // Rank 5: player_002 - 1800 (tied) // 注意:这里逻辑有误,player_002分数已更新,不应出现。 // 这个例子揭示了更新逻辑的一个潜在问题:我们按(score, uid)排序,但更新时只根据uid删除。 // 如果uid相同但score不同,旧的记录可能无法被正确找到和删除(因为find_if用的是uid,但multiset的序是score和uid)。 // 更健壮的做法是使用两个数据结构:一个set按(score,uid)排序用于排名,一个map(uid->iterator)用于快速查找。 return 0; }

这个例子揭示了使用单一multiset处理复杂更新逻辑时的挑战。对于需要频繁通过非排序键(如uid)进行查找和更新的场景,通常需要结合unordered_map来维护从键到迭代器的映射,以实现O(1)的查找和O(log n)的更新。

5.2 场景二:滑动窗口中的中位数查找

LeetCode上有一道经典题目“滑动窗口中位数”,multiset是其中一种高效的解法。它能够动态维护一个有序集合,并快速获取中位数。

#include <vector> #include <set> #include <iostream> #include <iterator> // for std::advance std::vector<double> medianSlidingWindow(const std::vector<int>& nums, int k) { std::multiset<int> window(nums.begin(), nums.begin() + k); auto mid = std::next(window.begin(), k / 2); // 指向“上半部分”的第一个元素 std::vector<double> medians; for (int i = k; ; ++i) { // 计算当前窗口中位数 if (k % 2 == 0) { // 偶数个元素,中位数是中间两个的平均值 auto mid_prev = std::prev(mid); medians.push_back((static_cast<double>(*mid_prev) + *mid) / 2.0); } else { // 奇数个元素,中位数就是中间那个 medians.push_back(*mid); } if (i == nums.size()) break; // 窗口滑动:删除左端元素,添加右端新元素 int toRemove = nums[i - k]; int toAdd = nums[i]; // 插入新元素 window.insert(toAdd); if (toAdd < *mid) { // 新元素插入在mid左侧,mid需要左移(因为集合大小增加了) --mid; } // 注意:必须先插入再处理删除,否则迭代器可能失效 // 删除旧元素 // 找到要删除元素的迭代器。由于有重复元素,需要找到正确的那个。 // 使用 lower_bound 找到第一个 >= toRemove 的位置 auto it_remove = window.lower_bound(toRemove); // 确保找到的元素就是我们要删除的值(理论上应该总是成立) if (it_remove != window.end() && *it_remove == toRemove) { if (it_remove == mid) { // 如果要删除的正好是mid,需要先移动mid再删除 mid = window.erase(it_remove); // erase返回被删除元素的下一个 // 但此时mid已经指向了下一个元素,对于奇数k,这可能是正确的,但需要调整 // 更安全的做法是:记录删除操作对mid的影响,然后统一调整mid // 这里简化处理,采用另一种策略:先调整mid,再删除 } else { if (toRemove <= *mid) { // 删除的元素在mid左侧(或就是mid),且不是mid本身,mid需要右移 ++mid; } window.erase(it_remove); } } // 上面的删除逻辑非常复杂且容易出错,特别是处理mid迭代器时。 // 更稳健的方法是维护两个multiset(大小平衡的),分别存储窗口的左半部分和右半部分。 // 这引出了下一个要点:使用multiset实现大小平衡堆。 } return medians; } // 注意:上述代码作为思路演示,其中删除逻辑存在缺陷,在实际应用或面试中需要更严谨的实现。

5.3 常见陷阱与性能坑点实录

  1. 迭代器失效的微妙之处:对于multiset,只有指向被删除元素的迭代器会失效。但是,如果你在循环中删除元素,需要特别注意。

    std::multiset<int> ms = {1, 2, 2, 3, 4}; // 错误示范:在基于范围的for循环中删除元素 for (auto it = ms.begin(); it != ms.end(); ++it) { if (*it == 2) { ms.erase(it); // 错误!erase(it)后,it失效,后续的++it是未定义行为 } } // 正确做法1:使用erase的返回值(它返回被删除元素之后元素的迭代器) for (auto it = ms.begin(); it != ms.end(); ) { if (*it == 2) { it = ms.erase(it); // 正确,it被更新为下一个有效位置 } else { ++it; } } // 正确做法2:C++11后,erase返回的迭代器指向被删元素的下一个 // 对于删除所有满足条件的元素,更简洁的做法是: ms.erase(2); // 直接删除所有值为2的元素
  2. 自定义比较函数的严格弱序:这是最容易出错的地方之一。比较函数必须满足严格弱序,否则会导致未定义行为,容器内部结构可能被破坏。

    // 错误示例:试图用 <= 来定义比较 struct BadCompare { bool operator()(int a, int b) const { return a <= b; // 违反了“不可自反性”,因为 a<=a 为 true。 } }; // 使用 BadCompare 实例化 multiset 是危险的。 // 正确做法:始终使用 < 或 > 来定义“小于”或“大于”关系。
  3. lower_boundupper_bound的误用:记住,lower_bound(k)返回的是第一个不小于k的元素位置(即>=k),upper_bound(k)返回的是第一个大于k的元素位置。对于multisetequal_range(k)通常比手动组合lower_bound/upper_bound更安全便捷。

  4. 性能误区:遍历查找替代find:由于multiset已排序,find是O(log n)的二分查找。绝对不要用线性遍历(O(n))来替代它。同样,count也是O(log n + k),其中k是重复元素个数,对于重复很多的情况,它可能接近O(n),此时如果需要遍历这些元素,直接用equal_range获取迭代器范围更高效。

  5. set的混淆导致逻辑错误:在需要元素唯一的场景误用了multiset,或者在允许重复的场景误用了set,都会导致数据丢失或逻辑错误。在设计阶段就要明确需求。

  6. 内存开销:如果存储的是小对象(如内置类型),multiset每个元素的节点开销(指针、颜色标记等)可能比数据本身还大。在内存敏感的场景,如果不需要实时排序,可以考虑用std::vector定期排序,或者用std::unordered_multiset

6. 进阶:基于Multiset实现可删除的优先队列(对顶堆)

我们之前提到了priority_queue不支持删除任意元素。结合multiset,我们可以实现一个功能更强大的“可删除优先队列”。

template<typename T, typename Compare = std::less<T>> class DeletablePriorityQueue { private: std::multiset<T, Compare> data; Compare comp; public: void push(const T& value) { data.insert(value); } void pop() { if (!data.empty()) { // 根据比较器决定弹出最大还是最小 if constexpr (std::is_same_v<Compare, std::less<T>>) { // 默认std::less,multiset升序,最大值在末尾 data.erase(std::prev(data.end())); } else { // 如果是std::greater,multiset降序,最小值在末尾 // 实际上,对于优先队列,我们通常想弹出“优先级最高”的。 // 假设Compare定义了“优先级低”的顺序,那么优先级最高的在begin()? // 这里需要根据Compare语义调整。一个更通用的方法是维护两个堆。 // 简化起见,我们假设Compare是std::less,弹出最大值。 data.erase(std::prev(data.end())); } } } const T& top() const { if constexpr (std::is_same_v<Compare, std::less<T>>) { return *std::prev(data.end()); } else { return *std::prev(data.end()); // 需要根据实际比较器调整 } } bool erase_one(const T& value) { auto it = data.find(value); if (it != data.end()) { data.erase(it); return true; } return false; } size_t erase_all(const T& value) { return data.erase(value); } bool empty() const { return data.empty(); } size_t size() const { return data.size(); } // 额外功能:获取迭代器,可用于遍历(但会破坏优先队列的抽象) auto begin() const { return data.begin(); } auto end() const { return data.end(); } }; int main() { // 一个最大堆(优先级高的值大) DeletablePriorityQueue<int> maxPQ; maxPQ.push(3); maxPQ.push(1); maxPQ.push(4); maxPQ.push(1); std::cout << "Top: " << maxPQ.top() << std::endl; // 4 maxPQ.erase_one(1); // 删除一个1 std::cout << "Size after erase one 1: " << maxPQ.size() << std::endl; // 3 maxPQ.pop(); std::cout << "Top after pop: " << maxPQ.top() << std::endl; // 3 return 0; }

这个实现虽然简单,但poptop操作是O(1)(通过访问首尾迭代器),pusherase是O(log n)。它比标准的priority_queue功能更强,但代价是每次插入删除都有O(log n)的树操作开销,而priority_queuepushpop是O(log n)的堆调整,通常常数更小。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/29 5:20:18

STM32F407串口DMA收发实战:标准库配置与环形缓冲区实现

1. 项目缘起&#xff1a;为什么STM32F407的串口DMA收发值得深究&#xff1f;最近在做一个基于STM32F407的数据采集转发项目&#xff0c;核心任务是把ADC采集到的大量数据通过串口实时、稳定地发送给上位机&#xff0c;同时还要能随时接收上位机的控制指令。一开始&#xff0c;我…

作者头像 李华
网站建设 2026/7/29 5:20:12

快速了解C语言基础(6)(数组初探)

数组数组&#xff1a;保存多个数据类型相同的数据。1)数组基础1.数组定义类类型说明符 数组名[常量];类型说明符&#xff1a;基本数据类型、构造数据类型(不能是void类型)。说明数组中保存的数据的数据类型。数组名&#xff1a;遵循标识符的命名规则。常量&#xff1a;数组中可…

作者头像 李华
网站建设 2026/7/29 5:15:14

STM32与A5000安全芯片的物联网设备开发实践

1. 硬件选型与安全架构设计在物联网设备开发中&#xff0c;选择STM32F100ZE作为主控芯片搭配A5000安全芯片的方案&#xff0c;主要基于以下考量&#xff1a;STM32F100ZE作为Cortex-M3内核微控制器&#xff0c;具有以下关键特性&#xff1a;72MHz主频和16KB SRAM&#xff0c;满足…

作者头像 李华
网站建设 2026/7/29 5:14:41

智能车编码器里程计:从硬件连接到软件实现的精准测距指南

1. 项目概述&#xff1a;为什么智能车需要精确的“里程计”&#xff1f;在智能车竞赛中&#xff0c;无论是摄像头循迹、电磁导航还是其他组别&#xff0c;让车“知道自己走了多远”都是一个基础且核心的需求。你可能遇到过这样的场景&#xff1a;车子在直道上跑得飞快&#xff…

作者头像 李华
网站建设 2026/7/29 5:13:53

DIY桌面级3D激光扫描仪:从硬件搭建到点云生成全流程解析

1. 项目概述&#xff1a;从零打造一台桌面级3D激光扫描仪几年前&#xff0c;当我第一次接触3D打印时&#xff0c;一个念头就挥之不去&#xff1a;如果能把自己手边的实物&#xff0c;比如一个精巧的模型、一件有纪念意义的小物件&#xff0c;轻松地转换成数字模型&#xff0c;那…

作者头像 李华