unordered_map 与 unordered_set 的使用
- 前言
- [1. unordered_set 的基本特性与去重无序演示](#1. unordered_set 的基本特性与去重无序演示)
- [2. unordered_set 类的模板参数剖析](#2. unordered_set 类的模板参数剖析)
- [3. unordered_set 与 set 的核心差异对比](#3. unordered_set 与 set 的核心差异对比)
- [4. unordered_map 与 map 的核心差异对比](#4. unordered_map 与 map 的核心差异对比)
- [5. 性能实测代码分析](#5. 性能实测代码分析)
- [6. 完整源码展示](#6. 完整源码展示)
- [7. 实例演示总结](#7. 实例演示总结)
前言
在 C++ STL 中,关联式容器是日常开发中不可或缺的利器。此前我们已经学习了基于红黑树的 set 和 map,它们能够保证元素有序且支持高效的对数时间复杂度操作。而今天,我们要探索的是基于哈希表实现的无序关联式容器:unordered_set 与 unordered_map。
我们将结合具体的测试代码,深入剖析它们的行为特征、核心差异以及性能表现。
1. unordered_set 的基本特性与去重无序演示
首先,我们通过一段直观的代码来观察 unordered_set 的基本行为。
cpp
void test_set1()
{
unordered_set<int> s = { 3,1,6,7,8,2,1,1,5,6,7,6 };
unordered_set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
⭐ 语法细节与行为机制:
去重(Deduplication):
从初始化列表 { 3,1,6,7,8,2,1,1,5,6,7,6 } 可以看出,其中包含了大量的重复元素(如多个 1 和 6)。但最终存入 unordered_set 后,所有重复值都会被自动过滤,只保留一个。
无序(Unordered):
通过迭代器遍历输出时,元素的顺序并不是按从小到大的升序排列,也和初始化的顺序无关,而是由底层哈希函数计算出的哈希地址决定的。
2. unordered_set 类的模板参数剖析
unordered_set 的类模板声明如下:
cpp
template < class Key, // unordered_set::key_type/value_type
class Hash = hash<Key>, // unordered_set::hasher
class Pred = equal_to<Key>, // unordered_set::key_equal
class Alloc = allocator<Key> // unordered_set::allocator_type
>
class unordered_set;
⭐ 语法细节与行为机制:
Key:底层关键字的类型。
Hash:第二个模板参数是一个仿函数,默认要求 Key 支持转换为整型(通过 std::hash)。
Pred:第三个模板参数是用于比较两个 Key 是否相等的仿函数(默认使用 std::equal_to)。
Alloc:第四个模板参数是空间配置器。
实际开发建议:在绝大多数常规应用场景下,我们都不需要手动传递后三个模板参数,直接使用默认值即可。
3. unordered_set 与 set 的核心差异对比
虽然 unordered_set 的核心接口(如 insert、erase、find)与 set 完全一致,但二者存在三项本质差异:
-
对 Key 的要求不同:
- set 要求 Key 支持小于比较(operator<)
- unordered_set 要求 Key 支持转成整型并支持等于比较(operator==),这本质上是哈希表底层存储和冲突解决的要求。
-
迭代器与遍历顺序的差异:
- set 的迭代器是双向迭代器,底层红黑树通过中序遍历保证有序+去重;
- 而 unordered_set 的迭代器是单向迭代器,底层哈希表由于哈希映射导致元素存储杂乱,遍历结果是无序+去重。
-
性能差异:
- 红黑树的增删查改效率为 O ( log N ) O(\log N) O(logN),
- 哈希表的增删查平均效率为 O ( 1 ) O(1) O(1)。
4. unordered_map 与 map 的核心差异对比
与 unordered_set 类似,unordered_map 与 map 也存在对应的特性差异:
-
增删查改接口(如 insert、erase、find 以及 mapped_type& operator\[\])用法完全一致。
-
map 要求 Key 支持小于比较且遍历结果有序(Key 有序+去重);unordered_map 要求 Key 支持转成整型与等于比较,遍历结果无序。
-
在绝大多数场景下,unordered_map 的增删查改速度明显更快。
5. 性能实测代码分析
为了直观感受 set 与 unordered_set 在海量数据下的性能差异,我们可以通过 test_set2 进行高强度压测。
cpp
void test_set2()
{
const size_t N = 10000000;
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. set 插入性能测试
size_t begin1 = clock();
for (auto e : v)
{
s.insert(e);
}
size_t end1 = clock();
cout << "set insert:" << end1 - begin1 << endl;
// 2. unordered_set 插入性能测试(配合 reserve)
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 << "插入数据个数:" << s.size() << endl;
cout << "插入数据个数:" << us.size() << endl << endl;
// 3. set 查找性能测试
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;
// 4. unordered_set 查找性能测试
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 << "unorered_set find:" << end4 - begin4 << "->" << m2 << endl;
// 5. set 删除性能测试
size_t begin5 = clock();
for (auto e : v)
{
s.erase(e);
}
size_t end5 = clock();
cout << "set erase:" << end5 - begin5 << endl;
// 6. unordered_set 删除性能测试
size_t begin6 = clock();
for (auto e : v)
{
us.erase(e);
}
size_t end6 = clock();
cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
}
🤔 疑问:为什么在测试 unordered_set 插入性能前需要显式调用 us.reserve(N)?这会对性能产生什么影响?
✅️答:unordered_set 底层采用哈希表 存储。
当插入大量数据(如 1000 万个元素)时,如果不提前通过 reserve 预留足够的桶空间,哈希表在元素增加的过程中会频繁触发自动扩容与 rehash(重新哈希) 。
每一次 rehash 都需要重新计算所有元素的哈希值并重新分配链表节点,这会带来巨大的时间开销。提前 reserve(N) 可以有效避免运行途中的多次动态扩容,从而压榨出哈希表 O ( 1 ) O(1) O(1) 的极致插入性能。
6. 完整源码展示
以下是包含上述所有测试函数与 main 函数的完整可运行 C++ 源码:
cpp
#include <iostream>
#include <vector>
#include <set>
#include <unordered_set>
#include <cstdlib>
#include <ctime>
using namespace std;
// 测试 1:演示 unordered_set 的去重与无序特性
void test_set1()
{
// 初始化包含重复元素的集合
unordered_set<int> s = { 3, 1, 6, 7, 8, 2, 1, 1, 5, 6, 7, 6 };
unordered_set<int>::iterator it = s.begin();
cout << "test_set1 遍历结果(无序且自动去重):" << endl;
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl << endl;
}
// 测试 2:海量数据下 set 与 unordered_set 的性能对比
void test_set2()
{
const size_t N = 10000000; // 1000万数据量
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()); // 随机值较多重复
//v.push_back(rand() + i); // 重复值相对少
v.push_back(i); // 没有重复,有序填充
}
// 1. 测试 set 插入性能(红黑树)
size_t begin1 = clock();
for (auto e : v)
{
s.insert(e);
}
size_t end1 = clock();
cout << "set insert time: " << end1 - begin1 << " ms" << endl;
// 2. 测试 unordered_set 插入性能(哈希表,预留空间优化)
size_t begin2 = clock();
us.reserve(N); // 预留哈希桶空间,避免频繁扩容
for (auto e : v)
{
us.insert(e);
}
size_t end2 = clock();
cout << "unordered_set insert time: " << end2 - begin2 << " ms" << endl;
cout << "set 插入数据个数:" << s.size() << endl;
cout << "unordered_set 插入数据个数:" << us.size() << endl << endl;
// 3. 测试 set 查找性能
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 time: " << end3 - begin3 << " ms -> found count: " << m1 << endl;
// 4. 测试 unordered_set 查找性能
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 time: " << end4 - begin4 << " ms -> found count: " << m2 << endl << endl;
// 5. 测试 set 删除性能
size_t begin5 = clock();
for (auto e : v)
{
s.erase(e);
}
size_t end5 = clock();
cout << "set erase time: " << end5 - begin5 << " ms" << endl;
// 6. 测试 unordered_set 删除性能
size_t begin6 = clock();
for (auto e : v)
{
us.erase(e);
}
size_t end6 = clock();
cout << "unordered_set erase time: " << end6 - begin6 << " ms" << endl << endl;
}
int main()
{
// 运行特性演示
test_set1();
// 运行海量数据性能对比
test_set2();
return 0;
}
7. 实例演示总结
通过本篇博客的学习与实战代码的验证,我们可以得出明确的容器选型指南:
选择 std::set / std::map:当你的业务场景明确要求元素保持有序,或者需要使用区间查询接口(如 lower_bound、upper_bound)时。
选择 std::unordered_set / std::unordered_map:当你的核心诉求是追求极致的查找、插入和删除效率,且对元素的顺序没有任何要求时,哈希结构的无序容器将带来断层式的性能提升!