C++ STL篇(十三) ------ unordered_set / unordered_map
前言:为什么我们需要"无序"?
在学习 C++ 的过程中,我们首先接触的关联式容器通常是 set 和 map。它们底层由红黑树 实现,能够自动将元素按照 key 的大小排序,遍历时得到一个有序序列,增删查改的时间复杂度稳定在 O(log N)。既然它又有序又稳定,为什么还要引入 unordered_set 和 unordered_map 呢?
答案藏在哈希表 里。对于大多数只需要"快速查找、快速插入、快速删除"的场景,我们并不关心元素是否有序。哈希表能够在平均 O(1) 的时间内完成这些操作,比 O(log N) 快得多。因此,C++11 引入了以哈希表为底层的无序关联容器,它们就是 unordered_set、unordered_map、unordered_multiset 和 unordered_multimap。
文章目录
- [C++ STL篇(十三) ------ unordered_set / unordered_map](#C++ STL篇(十三) —— unordered_set / unordered_map)
-
- 前言:为什么我们需要"无序"?
- [1. unordered_set 系列的使用](#1. unordered_set 系列的使用)
-
- [1.1 参考文档](#1.1 参考文档)
- [1.2 unordered_set 的声明与模板参数](#1.2 unordered_set 的声明与模板参数)
- [1.3 与 set 的差异:三个角度彻底看懂](#1.3 与 set 的差异:三个角度彻底看懂)
-
- [差异一:对 Key 的要求截然不同](#差异一:对 Key 的要求截然不同)
- 差异二:迭代器的区别
- 差异三:性能上的实质差异
- [1.4 使用接口对比](#1.4 使用接口对比)
- [2. unordered_map 的使用差异](#2. unordered_map 的使用差异)
-
- [2.1 与 map 的三个核心差异](#2.1 与 map 的三个核心差异)
- [2.2 使用接口对比](#2.2 使用接口对比)
- [3. unordered_multiset 与 unordered_multimap](#3. unordered_multiset 与 unordered_multimap)
- [4. 哈希相关接口:桶与负载因子](#4. 哈希相关接口:桶与负载因子)
-
- [4.1 Buckets(桶)相关](#4.1 Buckets(桶)相关)
- [4.2 Hash policy(哈希策略)](#4.2 Hash policy(哈希策略))
- 结语:
1. unordered_set 系列的使用
1.1 参考文档
传送门:cplusplus------unordered_set
1.2 unordered_set 的声明与模板参数
先来看 unordered_set 的完整声明:

虽然这四个模板参数看着有点吓人,但大部分时候我们只需要传第一个。
这里我们来拆解一下:
- Key :你想存储的元素类型,也就是关键字。在
unordered_set中,key 就是 value,因为集合里只存键,没有映射值。 - Hash = hash :把 Key 变成整数 的仿函数。哈希表需要用一个无符号整数来定位元素该放在哪个桶里,因此要求 Key 能转换为整数 。标准库已经为
int、string、指针等常用类型实现了hash,如果你用自定义类型,就需要自己写一个仿函数,让它可以计算出哈希值。 - Pred = equal_to :判断两个 Key 是否相等 的仿函数。哈希表定位到桶之后,还要在桶内逐个比较元素才能确认是同一个 key。默认用
equal_to<Key>,也就是调用operator==。 - Alloc = allocator:空间配置器,负责内存的申请与释放。一般不用管,除非你想做内存池优化。
1.3 与 set 的差异:三个角度彻底看懂
差异一:对 Key 的要求截然不同
set要求 Key 支持小于比较 (operator<),因为它底层红黑树要靠比较来维持有序。unordered_set要求 Key 支持转换成整数 (哈希) 和相等比较 (operator==),这是哈希表底层原理决定的。
差异二:迭代器的区别
- 迭代器类型 :
set::iterator是双向 迭代器,可以++、--;unordered_set::iterator是单向 迭代器,只能++。因为哈希桶通常是用单链表串起来的,不支持逆向遍历。 - 遍历顺序 :
set遍历是中序遍历,元素按 key 升序 输出,自带去重功能。unordered_set遍历没有任何顺序保证,它取决于哈希值和桶的分布,输出是无序的,但也去重。
差异三:性能上的实质差异
- 红黑树的增删查都是 O(log N)。
- 哈希表的平均 复杂度是 O(1),最坏情况下退化为 O(N)(当所有元素挤在同一个桶里时)。
实际使用中,通过良好的哈希函数和控制负载因子 (元素个数 / 桶个数),我们能保证它接近 O(1)。因此,在绝大部分场景下,unordered_set的增删查改比set要快很多。
下面用一段代码来直观感受这些差异。
cpp
void test_set1()
{
const size_t N = 1000000; // 一百万
unordered_set<int> us;
set<int> s;
vector<int> v;
v.reserve(N);
srand(time(0));
// 随机数种子,使每次运行生成不同随机序列
for (size_t i = 0; i < N; ++i)
{
// v.push_back(rand()); // N比较大时重复值较多
v.push_back(rand() + i); // 重复值较少
// v.push_back(i); // 无重复,有序
}
// 1. 插入测试
size_t begin1 = clock();
for (auto e : v)
{
s.insert(e);
}
size_t end1 = clock();
cout << "set insert: " << end1 - begin1 << endl;
size_t begin2 = clock();
us.reserve(N);
for (auto e : v)
{
us.insert(e);
}
size_t end2 = clock();
cout << "unordered_set insert: " << end2 - begin2 << endl;
// 实际插入的元素个数
cout << "插入数据个数(set): " << s.size() << endl;
cout << "插入数据个数(unordered_set): " << us.size() << endl << endl;
// 2. 查找测试
int m1 = 0;
size_t begin3 = clock();
for (auto e : v)
{
auto ret = s.find(e);
if (ret != s.end()) ++m1;
}
size_t end3 = clock();
cout << "set find: " << end3 - begin3 << " -> " << m1 << endl;
int m2 = 0;
size_t begin4 = clock();
for (auto e : v)
{
auto ret = us.find(e);
if (ret != us.end()) ++m2;
}
size_t end4 = clock();
cout << "unordered_set find: " << end4 - begin4 << " -> " << m2 << endl << endl;
// 3. 删除测试
size_t begin5 = clock();
for (auto e : v)
{
s.erase(e);
}
size_t end5 = clock();
cout << "set erase: " << end5 - begin5 << endl;
size_t begin6 = clock();
for (auto e : v)
{
us.erase(e);
}
size_t end6 = clock();
cout << "unordered_set erase: " << end6 - begin6 << endl << endl;
}
效果:

运行结果(你的环境可能不同):
set insert耗时可能是unordered_set insert的两到三倍。- 查找和删除也有类似差距,
unordered_set明显更快。
1.4 使用接口对比

2. unordered_map 的使用差异
参考文档
传送门:cplusplus------unordered_map

unordered_map 与 unordered_set 几乎一样,唯一的区别是:它存储的是键值对 pair<const Key, T> 。所以它的 Key 要求、迭代器性质、性能特征与 unordered_set 完全一致,只是多了一个映射值 mapped_type。
2.1 与 map 的三个核心差异
- Key 的要求 :
map要求Key支持<比较;unordered_map要求Key支持哈希转换和相等比较。 - 迭代器 :
map是双向迭代器,遍历 Key 升序 ;unordered_map是单向迭代器,遍历 无序。 - 性能 :大多数情况下
unordered_map增删查更快,尽管复杂度为平均 O(1),但哈希表常数略大,若数据量很小,红黑树可能反而显得更快。一般当 N > 几百时,哈希优势开始显现。
2.2 使用接口对比

3. unordered_multiset 与 unordered_multimap


- 允许键重复 :与
multiset/multimap类似,这两个容器允许插入重复的键。 - 差异依然是三点 :对 key 的要求(哈希+判等)、迭代器单向遍历无序、性能平均 O(1)。
- 使用时注意
insert总是成功,返回一个迭代器(不是 pair),因为不存在重复导致的失败。 - 统计某个键的个数用
count(key),它会返回该键出现的次数。

4. 哈希相关接口:桶与负载因子
unordered_set 和 unordered_map 提供了许多与哈希表底层相关的接口,虽然平常不常用,但了解它们有助于理解性能调优。这些接口在文档中分为 Buckets 和 Hash policy 两组。
4.1 Buckets(桶)相关
bucket_count():返回当前桶的总数。max_bucket_count():系统允许的最大桶数。bucket_size(n):第 n 个桶中元素的个数。bucket(const key_type& k):返回键 k 会被放在哪个桶(桶索引)。
这些接口可以帮助你分析哈希分布是否均匀。例如遍历所有桶,统计每个桶的大小,如果发现个别桶特别长,说明哈希函数不够均匀,或者负载因子太高。
4.2 Hash policy(哈希策略)
load_factor():当前负载因子,计算公式为size() / bucket_count()。max_load_factor():获取或设置最大负载因子 。当load_factor()超过该值时,容器会自动rehash,增加桶的数量并重新分配所有元素。rehash(count):手动触发 rehash,将桶数调整为至少count。reserve(n):为至少容纳n个元素预留桶。
结语:
因为之前已经详细讲解过 map和set 所以本文对一些重复度较高的部分就没有过多赘述,有需要的小伙伴可以看看博主往期的文章。꜀(˘꒳˘ ꜀)
【C++ STL篇(八)】set容器------零基础入门与核心用法精讲
【C++ STL篇(九)】map容器------零基础入门与核心用法精讲
【C++ STL篇(十二)】红黑树の影分身:一棵树如何同时化身 map 和 set(万字拆解+源码)
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _