深入解析 C++ unordered_set 与 unordered_map 的核心特性与性能实战

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。

我们将结合具体的测试代码,深入剖析它们的行为特征、核心差异以及性能表现。

unordered_set 的参考文档

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 完全一致,但二者存在三项本质差异:

  1. 对 Key 的要求不同:

    • set 要求 Key 支持小于比较(operator<)
    • unordered_set 要求 Key 支持转成整型并支持等于比较(operator==),这本质上是哈希表底层存储和冲突解决的要求。
  2. 迭代器与遍历顺序的差异:

    • set 的迭代器是双向迭代器,底层红黑树通过中序遍历保证有序+去重;
    • 而 unordered_set 的迭代器是单向迭代器,底层哈希表由于哈希映射导致元素存储杂乱,遍历结果是无序+去重。
  3. 性能差异:

    • 红黑树的增删查改效率为 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:当你的核心诉求是追求极致的查找、插入和删除效率,且对元素的顺序没有任何要求时,哈希结构的无序容器将带来断层式的性能提升!

相关推荐
码匠许师傅1 小时前
【C++三方组件】FFmpeg:音视频处理的瑞士军刀
c++·ffmpeg·音视频
ShineWinsu2 小时前
对于Qt:QPushButton常见属性的解析
开发语言·c++·qt·api·gui·按钮·qpushbutton
外贸GEO优化2 小时前
外贸独立站的可抓取性:JS 渲染、懒加载与无限滚动到底影响什么
开发语言·javascript·ecmascript
飞浪纪元[FWC–FE]2 小时前
Qt 工程创建与 CMake / qmake 构建规范
开发语言·数据库·qt
Lhan.zzZ2 小时前
用 Qt 构建线程安全的通讯静态库:从线程亲和性到完整工程
c++·vscode·qt·tcp/ip·udp
后台模板学习2 小时前
Go Web开发中任务调度框架的源码解析与高可用实现方案
开发语言·后端·golang
xiaoye-duck2 小时前
《C++ 项目:手写大模型接入 SDK》DeepSeek接口设计(上):初始化和全量响应
开发语言·网络·c++·语言模型
fpcc2 小时前
跟我学c++中级篇—streambuf在glog中的应用分析
开发语言·c++