std::set、std::multiset 和 std::unordered_set的异同
目录
1.简介
2.详细用法
2.1.初始化与构造
2.2.插入元素(insert)
2.3.查找元素(find)
2.4.删除元素(erase)
2.5.遍历元素
6.特有方法(因有序性差异)
3.自定义类型的支持
4.关键差异总结
5.总结
1.简介
std::set、std::multiset 和 std::unordered_set 都是 C++ 标准库中用于存储元素的容器,但它们在元素唯一性、有序性、底层实现和性能特性上有显著差异。
先通过表格直观对比三者的核心差异:
| 容器 | 底层实现 | 有序性 | 元素唯一性 | 插入 / 删除 / 查找复杂度 | 头文件 | 典型用途 |
|---|---|---|---|---|---|---|
std::set | 平衡二叉搜索树(红黑树) | 有序(默认升序) | 唯一 | O(log n) | <set> | 有序去重场景(如排序后的唯一值) |
std::multiset | 平衡二叉搜索树(红黑树) | 有序(默认升序) | 允许重复 | O(log n) | <set> | 有序允许重复场景(如统计频率前的排序) |
std::unordered_set | 哈希表(链地址法) | 无序 | 唯一 | 平均 O (1),最坏 O (n) | <unordered_set> | 无序去重场景(如快速判断元素是否存在) |
2.详细用法
以 int 类型为例,三者的接口设计相似,但因特性不同,部分操作的行为和返回值有差异。
2.1.初始化与构造
#include <set>
#include <unordered_set>
#include <iostream>int main() {// std::set:自动去重并排序std::set<int> s = {3, 1, 4, 1, 2}; // 结果:{1, 2, 3, 4}(升序,去重)// std::multiset:允许重复,自动排序std::multiset<int> ms = {3, 1, 4, 1, 2}; // 结果:{1, 1, 2, 3, 4}(升序,保留重复)// std::unordered_set:自动去重,无序std::unordered_set<int> us = {3, 1, 4, 1, 2}; // 结果:{3,1,4,2}(顺序不确定,去重)// 范围构造(从其他容器迭代器构造)std::set<int> s2(s.begin(), s.end()); // 复制 s 的元素(有序)std::multiset<int> ms2(us.begin(), us.end()); // 复制 us 的元素并排序:{1,2,3,4}return 0;
}
std::set、std::multiset 和 std::unordered_set为什么可以直接用{}去初始化,它的原因可参考:
C++之std::initializer_list详解
2.2.插入元素(insert)
插入是三者差异较明显的操作,主要体现在返回值和重复元素的处理上。
| 容器 | 插入行为 | 返回值类型 | 说明 |
|---|---|---|---|
std::set | 若元素已存在则插入失败,否则成功 | pair<iterator, bool> | second 为 true 表示插入成功 |
std::multiset | 无论元素是否存在都插入(允许重复) | iterator | 返回指向新插入元素的迭代器 |
std::unordered_set | 若元素已存在则插入失败,否则成功 | pair<iterator, bool> | 与 set 逻辑相同,但底层是哈希操作 |
代码示例:
// std::set 插入
auto ret_s = s.insert(5); // s 变为 {1,2,3,4,5},ret_s.second = true
auto ret_s2 = s.insert(3); // 3 已存在,ret_s2.second = false,ret_s2.first 指向已有 3// std::multiset 插入
auto it_ms = ms.insert(1); // ms 变为 {1,1,1,2,3,4},it_ms 指向新插入的 1(第二个 1 之后)// std::unordered_set 插入
auto ret_us = us.insert(5); // us 新增 5(位置不确定),ret_us.second = true
auto ret_us2 = us.insert(3); // 3 已存在,ret_us2.second = false
插入元素函数返回pair<iterator, bool>也是编程中返回值的常见用法,它的原型:
template <bool _Multi2 = _Multi, enable_if_t<!_Multi2, int> = 0>
pair<iterator, bool> insert(const value_type& _Val) {const auto _Result = _Emplace(_Val);return {iterator(_Result.first, _Get_scary()), _Result.second};
}
向容器中插入元素,若元素已存在则插入失败,返回包含结果的迭代器和标志。通过模板参数和 SFINAE 机制,它与多值容器(std::multiset)的 insert 实现(返回 iterator 而非 pair)区分开,确保不同容器的行为符合其设计语义(单值容器需返回是否插入成功,多值容器无需返回标志,因为允许重复插入)。
C++之std::enable_if
C++惯用法: 通过std::decltype来SFINAE掉表达式
利用std::set和std::unordered_set的有序可以实现判断一个序列是否有重复的元素,如:
#include <map>
#include <unordered_set>
#include <iostream>// 模板函数:支持任意key和value类型的map
template <typename K, typename V>
bool hasDuplicateValues(const std::map<K, V>& targetMap) {std::unordered_set<V> seenValues; // 记录已出现的valuefor (const auto& pair : targetMap) { // 遍历map的所有键值对// 尝试插入value,若插入失败(已存在),直接返回true(有重复)if (!seenValues.insert(pair.second).second) {return true;}}return false; // 遍历结束无重复
}// 测试示例
int main() {std::map<int, std::string> map1 = {{1, "a"}, {2, "b"}, {3, "a"}};std::map<int, int> map2 = {{1, 10}, {2, 20}, {3, 30}};std::cout << "map1是否有重复value:" << (hasDuplicateValues(map1) ? "是" : "否") << std::endl; // 输出“是”std::cout << "map2是否有重复value:" << (hasDuplicateValues(map2) ? "是" : "否") << std::endl; // 输出“否”return 0;
}
2.3.查找元素(find)
查找元素是否存在,返回指向元素的迭代器;若不存在,返回 end()。
std::set和std::multiset:因底层是红黑树,查找基于键的比较(有序查找)。std::unordered_set:基于哈希值查找(无序)。
// std::set 查找
auto it_s = s.find(3);
if (it_s != s.end()) {std::cout << "set 找到 3" << std::endl; // 存在,输出
}// std::multiset 查找(返回第一个匹配元素)
auto it_ms = ms.find(1);
if (it_ms != ms.end()) {std::cout << "multiset 找到第一个 1" << std::endl; // 存在,输出
}// std::unordered_set 查找
auto it_us = us.find(2);
if (it_us != us.end()) {std::cout << "unordered_set 找到 2" << std::endl; // 存在,输出
}
-
std::multiset允许值重复,因此可以用equal_range来方法专门用于返回这个连续范围,其返回值是一个pair<iterator, iterator>:
#include <set>
#include <iostream>int main() {std::multiset<int> ms = {1, 3, 3, 2, 3, 4}; // 有序存储:{1,2,3,3,3,4}int target = 3;// 获取所有值为 target 的元素范围auto range = ms.equal_range(target);// 遍历范围,输出所有相同元素std::cout << "值为 " << target << " 的所有元素:";for (auto it = range.first; it != range.second; ++it) {std::cout << *it << " "; // 输出:3 3 3}// (可选)获取相同元素的数量size_t count = std::distance(range.first, range.second);std::cout << "\n数量:" << count << std::endl; // 输出:3return 0;
}
2.4.删除元素(erase)
支持按值、迭代器或范围删除,差异主要体现在删除重复元素时的行为。
| 容器 | 按值删除(erase(val))返回值 | 说明 |
|---|---|---|
std::set | size_t(0 或 1) | 最多删除 1 个元素(因元素唯一) |
std::multiset | size_t(删除的元素个数) | 删除所有值为 val 的元素(可能多个) |
std::unordered_set | size_t(0 或 1) | 最多删除 1 个元素(因元素唯一) |
// std::set 删除
size_t cnt_s = s.erase(3); // 删除 3,返回 1(s 变为 {1,2,4,5})// std::multiset 删除(删除所有 1)
size_t cnt_ms = ms.erase(1); // ms 原 {1,1,1,2,3,4},删除后 {2,3,4},返回 3// std::unordered_set 删除
size_t cnt_us = us.erase(3); // 删除 3,返回 1
2.5.遍历元素
遍历行为受 “有序性” 影响:
std::set和std::multiset:遍历结果为升序(可通过自定义比较器修改顺序)。std::unordered_set:遍历结果无序(取决于哈希函数和插入顺序)。
// 遍历 set(有序)
std::cout << "set 遍历:";
for (int num : s) { std::cout << num << " "; } // 输出:1 2 4 5// 遍历 multiset(有序,可能有重复)
std::cout << "\nmultiset 遍历:";
for (int num : ms) { std::cout << num << " "; } // 输出:2 3 4// 遍历 unordered_set(无序)
std::cout << "\nunordered_set 遍历:";
for (int num : us) { std::cout << num << " "; } // 输出:1 2 4 5(顺序不确定)
6.特有方法(因有序性差异)
std::set 和 std::multiset(有序):提供范围查询方法(基于红黑树的有序特性):
lower_bound(val):返回第一个不小于val的元素迭代器。upper_bound(val):返回第一个大于val的元素迭代器。equal_range(val):返回包含所有等于val的元素的范围(set中范围长度为 0 或 1,multiset中可能更长)。
std::set<int> s = {1,3,5,7};
auto it_low = s.lower_bound(4); // 指向 5(第一个 >=4 的元素)
auto it_high = s.upper_bound(5); // 指向 7(第一个 >5 的元素)std::multiset<int> ms = {1,2,2,3};
auto range = ms.equal_range(2); // 范围为 [第一个 2, 第一个 >2 的元素),即两个 2
std::unordered_set(无序):提供哈希表相关方法:
load_factor():返回负载因子(元素数 / 桶数,影响哈希冲突概率)。rehash(n):调整桶数为至少n,减少哈希冲突。
3.自定义类型的支持
容器存储自定义类型时,需根据底层实现满足不同条件:
1.std::set 和 std::multiset(红黑树)
需定义比较规则(默认使用 std::less<T>,即需要 operator< 或自定义比较器),用于红黑树的排序。
#include <set>
#include <string>struct Student {std::string name;int score;
};// 自定义比较器:按分数升序(分数相同则按姓名升序)
struct CompareStudent {bool operator()(const Student& a, const Student& b) const {if (a.score != b.score) return a.score < b.score;return a.name < b.name;}
};// 声明容器时指定比较器
std::set<Student, CompareStudent> s_stu;
std::multiset<Student, CompareStudent> ms_stu;// 插入元素(会按分数自动排序)
s_stu.insert({"Alice", 90});
s_stu.insert({"Bob", 85}); // 按分数排序:Bob(85) -> Alice(90)
2.std::unordered_set(哈希表)
需定义哈希函数(std::hash<T> 特化)和相等判断(operator==),用于哈希表的存储和查找。
#include <unordered_set>struct Student {std::string name;int score;// 相等判断:姓名和分数都相同才视为相等bool operator==(const Student& other) const {return name == other.name && score == other.score;}
};// 特化 std::hash 提供哈希函数
namespace std {template<> struct hash<Student> {size_t operator()(const Student& s) const {size_t h1 = hash<std::string>()(s.name);size_t h2 = hash<int>()(s.score);return h1 ^ (h2 << 1); // 组合哈希值(简单实现)}};
}// 声明容器(自动使用自定义哈希和 ==)
std::unordered_set<Student> us_stu;// 插入元素(无序存储)
us_stu.insert({"Alice", 90});
us_stu.insert({"Bob", 85});
4.关键差异总结
1.元素唯一性
set和unordered_set不允许重复元素(插入重复值会失败)。multiset允许重复元素(插入重复值会成功,保留所有副本)。
2.有序性
set和multiset是有序容器(默认升序,可通过比较器修改),遍历结果有序。unordered_set是无序容器,遍历结果与插入顺序无关(取决于哈希函数)。
3.性能
set和multiset:插入、删除、查找的时间复杂度为 O(log n)(红黑树的平衡特性保证)。unordered_set:平均时间复杂度为 O(1),但最坏情况(哈希冲突严重)为 O(n)(取决于哈希函数质量)。
4.内存开销
set和multiset:红黑树需要存储额外的指针(父、左、右孩子),内存开销较大。unordered_set:哈希表需要存储桶和链表指针,内存开销与负载因子相关(负载因子过高会触发扩容)。
5.总结
- 若需要有序且唯一的元素,且可能需要范围查询(如找大于
x的元素):选std::set。 - 若需要有序且允许重复的元素,且需统计或处理重复值:选
std::multiset。 - 若不需要有序,仅需快速判断元素是否存在(插入 / 查找频繁):选
std::unordered_set(性能更优)。
通过以上对比,可根据具体需求(有序性、重复值允许、性能优先级)选择最合适的容器。
相关文章:
C++之multimap:关键字分类的利器
C++ STL中 set 和 map 的区别
