深入解析C++ STL:set与multiset容器
·
好的,我们来深入探讨C++标准模板库(STL)中的set和multiset容器,重点关注其接口使用和核心特性。
一、核心概念与特性
-
有序关联容器:
set和multiset都是有序的关联容器。它们存储元素(键),并根据元素自身的值(或指定的比较函数)进行自动排序。- 元素在容器中总是保持特定的顺序(默认为升序),这使得基于范围的查询(如
lower_bound,upper_bound)非常高效。
-
键值唯一性 - 核心区别:
set:容器中的键值必须是唯一的。尝试插入一个已存在的键值会被忽略(或返回失败信息)。multiset:允许键值重复。可以存储多个具有相同值的元素。
-
底层数据结构:
- 两者通常基于红黑树(Red-Black Tree) 实现。红黑树是一种自平衡的二叉搜索树(BST)。
- 平衡性:红黑树通过特定的着色规则和旋转操作,保证了树在插入和删除操作后仍能大致平衡,从而确保了最坏情况下插入、删除和查找操作的时间复杂度为$O(\log n)$。
-
不可修改键值:
- 由于元素在容器中的位置由其键值决定(用于排序),一旦元素被插入到
set或multiset中,其键值(即元素本身)就不能被直接修改。 - 修改键值会破坏容器的有序性。如果需要修改元素,通常的做法是先删除旧元素,再插入新元素。
- 由于元素在容器中的位置由其键值决定(用于排序),一旦元素被插入到
-
性能特点:
- 查找:$O(\log n)$(得益于二叉搜索树特性)
- 插入:$O(\log n)$(查找插入位置 + 可能的树平衡操作)
- 删除:$O(\log n)$(查找元素 + 可能的树平衡操作)
- 范围查询:$O(\log n + k)$,其中$k$是范围内元素的数量(因为元素在树中是按顺序存储的,遍历子树即可)。
- 内存:每个元素需要存储额外的信息(如颜色标记、父/子指针),因此空间开销比顺序容器(如
vector)大。
二、关键接口使用
-
构造与赋值:
std::set<int> s1; // 空set,使用默认比较器 (operator<) std::set<int, std::greater<int>> s2; // 使用std::greater排序,降序 std::set<int> s3 = {1, 3, 2}; // 初始化列表,元素自动排序 {1, 2, 3} std::multiset<int> ms1(s3.begin(), s3.end()); // 用迭代器范围构造multiset s1 = s3; // 赋值 -
插入元素:
set:std::pair<iterator, bool> insert(const value_type& val); // 尝试插入val。返回一个pair: // pair.first: 指向被插入元素(或阻止插入的已有元素)的迭代器 // pair.second: bool值,true表示插入成功,false表示键已存在(插入被阻止) auto ret = s1.insert(4); // 插入新元素4, ret.second == true ret = s1.insert(3); // 尝试插入已存在的3, ret.second == falsemultiset:iterator insert(const value_type& val); // 总是插入val。返回指向新插入元素的迭代器。 auto it = ms1.insert(3); // 成功插入一个3,返回指向这个新3的迭代器
-
查找元素:
find(key):查找键等于key的元素,返回指向它的迭代器。如果没找到,返回end()。时间复杂度$O(\log n)$。auto it = s1.find(2); // 找到元素2 if (it != s1.end()) { /* 找到了 */ }count(key):set:返回0或1(因为键唯一)。multiset:返回键等于key的元素个数。时间复杂度$O(\log n + count)$。
int cnt = ms1.count(3); // 统计值为3的元素个数
-
范围查询:
lower_bound(key):返回指向第一个键不小于key的元素的迭代器。upper_bound(key):返回指向第一个键大于key的元素的迭代器。equal_range(key):返回一个pair<iterator, iterator>,表示键等于key的元素范围([first, last))。对于set,这个范围最多包含一个元素(如果存在)。// 在s1中查找所有 >= 2 且 < 4 的元素 auto low = s1.lower_bound(2); auto high = s1.upper_bound(4); for (auto it = low; it != high; ++it) { std::cout << *it << " "; } // 或者使用 equal_range (对于multiset更常用) auto range = ms1.equal_range(3); for (auto it = range.first; it != range.second; ++it) { std::cout << *it << " "; // 输出所有3 }
-
删除元素:
erase(iterator pos):删除迭代器pos指向的元素。返回指向被删除元素之后元素的迭代器(或end())。erase(key):删除所有键等于key的元素(对于set最多删一个)。set:返回删除的元素个数(0或1)。multiset:返回删除的元素个数。
erase(iterator first, iterator last):删除[first, last)范围内的元素。s1.erase(3); // 删除键为3的元素(如果存在) auto it = s1.find(2); if (it != s1.end()) { s1.erase(it); // 通过迭代器删除 } ms1.erase(ms1.lower_bound(3), ms1.upper_bound(3)); // 删除multiset中所有3
-
大小与容量:
size():返回容器中元素的数量。empty():判断容器是否为空。max_size():返回容器理论上能容纳的最大元素数(通常很大)。
-
迭代器:
- 提供双向迭代器(
begin(),end(),cbegin(),cend(),rbegin(),rend(),crbegin(),crend())。 - 迭代器按排序顺序遍历元素(升序或降序,取决于比较器)。
- 注意:删除或插入元素可能使指向其他元素的迭代器失效(但指向被删除元素的迭代器总是失效)。指向未修改元素的迭代器通常不会失效(红黑树实现保证了这点)。
- 提供双向迭代器(
三、示例代码
#include <iostream>
#include <set>
#include <string>
#include <functional> // for std::greater
int main() {
// Set (唯一键,升序)
std::set<int> uniqueSet = {5, 2, 8, 2, 1}; // {1, 2, 5, 8}
uniqueSet.insert(3); // 插入成功
uniqueSet.insert(2); // 插入失败,键已存在
std::cout << "Set contains: ";
for (int elem : uniqueSet) {
std::cout << elem << " "; // 输出: 1 2 3 5 8
}
std::cout << "\nCount of 2 in set: " << uniqueSet.count(2) << "\n"; // 输出: 1
// Multiset (允许重复键,降序)
std::multiset<int, std::greater<int>> multiSet = {5, 2, 8, 2, 1}; // {8, 5, 2, 2, 1}
multiSet.insert(2); // 插入成功,现在有三个2
std::cout << "Multiset contains: ";
for (int elem : multiSet) {
std::cout << elem << " "; // 输出: 8 5 2 2 2 1
}
std::cout << "\nCount of 2 in multiset: " << multiSet.count(2) << "\n"; // 输出: 3
// 范围查询 (在multiset中查找所有等于2的元素)
auto range = multiSet.equal_range(2);
std::cout << "All '2's: ";
for (auto it = range.first; it != range.second; ++it) {
std::cout << *it << " "; // 输出: 2 2 2
}
std::cout << "\n";
// 删除multiset中所有2
multiSet.erase(2);
std::cout << "Count of 2 after erase: " << multiSet.count(2) << "\n"; // 输出: 0
return 0;
}
四、总结与提示
- 选择依据:需要键唯一且有序,用
set;需要有序但允许重复键,用multiset。 - 性能优势:在需要频繁查找、插入、删除且维护元素顺序的场景下表现优异($O(\log n)$)。
- 键不可变:记住不能直接修改容器中的键值。
- 自定义类型:如果元素是自定义类型(类或结构体),需要提供比较函数或重载
operator<(或指定自定义比较器)。 - 迭代器失效:删除操作会使指向被删除元素的迭代器失效。插入操作通常不会使其他迭代器失效(红黑树特性)。
- 替代方案:如果不需要顺序,只关心存在性,考虑
unordered_set/unordered_multiset(哈希表实现,平均$O(1)$操作,但不保证顺序)。
通过理解其底层红黑树实现和提供的接口,你可以更有效地在C++程序中使用set和multiset来解决需要有序存储和高效查询的问题。
更多推荐


所有评论(0)