简单聊聊 C++ 中的 unordered_map 和 unordered_set

C++ 标准库提供了两大类关联式容器:一类是有序 的,比如 setmap,底层用红黑树实现;另一类是无序 的,也就是今天要聊的 unordered_setunordered_mapunordered_multisetunordered_multimap,它们的底层依靠哈希表

如果你的需求只是"快速判断一个值在不在""根据键快速查到值",不在乎元素是否排序,那么无序容器通常是更高效的选择。下面我会从最基础的 unordered_set 开始,把它们的用法、和有序容器的区别,以及一些容易被忽略的细节完整串一遍。


目录

[1. unordered_set 是什么](#1. unordered_set 是什么)

[2. unordered_set 和 set 的三个主要差异](#2. unordered_set 和 set 的三个主要差异)

[2.1 对键的要求不同](#2.1 对键的要求不同)

[2.2 迭代器和遍历顺序不同](#2.2 迭代器和遍历顺序不同)

[2.3 性能差异](#2.3 性能差异)

[3. unordered_map 和 map 的差异](#3. unordered_map 和 map 的差异)

[4. 允许重复的版本:unordered_multiset 与 unordered_multimap](#4. 允许重复的版本:unordered_multiset 与 unordered_multimap)

[5. 一些和哈希实现相关的接口](#5. 一些和哈希实现相关的接口)

[6. 小结](#6. 小结)


1. unordered_set 是什么

unordered_set 是一个不重复元素集合 ,但与 set 不同,它里面的元素没有顺序。它的声明大致如下:

cpp

复制代码
template <
    class Key,                     // 元素类型
    class Hash = hash<Key>,        // 哈希函数
    class Pred = equal_to<Key>,    // 判断相等的方式
    class Alloc = allocator<Key>   // 空间配置器
> class unordered_set;

对大多数日常使用,后面三个模板参数都用默认值就行。它们的作用是:

  • Hash :把 Key 类型的值转换成一个整数(哈希值),默认的 hash<Key> 支持了 intstring 等常见类型。如果你的自定义类型作为键,就需要自己实现哈希函数传给这个参数。

  • Pred :判断两个键是否相等,默认为 equal_to<Key>,也就是用 ==。这和 set< 比较完全不同,后面会细说。

  • Alloc:底层内存分配方式,一般不需要动。

unordered_set 的增删查平均时间复杂度都是 O(1),最坏情况可能退化,但合理使用下很少遇到。因为它不排序,所以迭代器遍历出的元素是无序的,并且自动去重。


2. unordered_set 和 set 的三个主要差异

很多教程光说"一个有序一个无序",但其实差异不止这一点。把它们理清,你就能在合适的场景选对容器。

2.1 对键的要求不同

  • set 要求元素支持小于比较operator<),因为它要维持二叉搜索树的有序性。

  • unordered_set 要求元素支持哈希计算 (转成整数)和相等比较operator==)。换句话说,元素必须能算出哈希值,并且在哈希冲突时能判断两个元素是不是真的相等。

这是由底层数据结构决定的:红黑树靠比较来决定走左子树还是右子树,哈希表则靠哈希值分散位置,冲突后用相等判断来确认。

2.2 迭代器和遍历顺序不同

  • set 提供的是双向迭代器 ,可以 ++ 也可以 --,因为它内部是红黑树,中序遍历天然得到升序排列。

  • unordered_set 提供的是单向迭代器 ,只支持 ++。由于底层是哈希桶,元素顺序由哈希值决定,遍历出来的顺序既不是插入顺序,也不是大小顺序,完全无序。

小结一下:set:有序 + 去重;unordered_set:无序 + 去重。

2.3 性能差异

这是最吸引人去用无序容器的一点:大多数场景下,unordered_set 的增删查比 set 更快。

  • 红黑树的增删查复杂度是 O(log N)

  • 哈希表的增删查平均复杂度是 O(1)

我们用一段比较极端的测试代码来看看差距(代码逻辑很简单,就是对两个容器分别做 100 万次插入、查找、删除,并计时):

cpp

复制代码
#include <unordered_set>
#include <set>
#include <vector>
#include <iostream>
#include <ctime>
using namespace std;

int main() {
    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() + i);
    
    // 插入
    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;
    
    // 查找
    int m1 = 0, m2 = 0;
    size_t begin3 = clock();
    for (auto e : v) if (s.find(e) != s.end()) ++m1;
    size_t end3 = clock();
    cout << "set find: " << end3 - begin3 << " -> matched " << m1 << endl;
    
    size_t begin4 = clock();
    for (auto e : v) if (us.find(e) != us.end()) ++m2;
    size_t end4 = clock();
    cout << "unordered_set find: " << end4 - begin4 << " -> matched " << m2 << endl;
    
    // 删除
    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;
    
    cout << "set size: " << s.size() << endl;
    cout << "unordered_set size: " << us.size() << endl;
    
    return 0;
}

在我的运行环境里,unordered_set 在该测试中的插入、查找、删除都明显快于 set。需要注意,这里在插入前调用了 us.reserve(N),作用是提前分配哈希表的桶,避免反复扩容带来的开销,这个优化对有序容器没有,但对无序容器很受用。

小贴士:理论上如果数据分布极端、哈希函数很差,哈希表可能退化到 O(N)。但标准库自带的 hash 对付普通整数和字符串绰绰有余,一般不用太担心。


3. unordered_map 和 map 的差异

unordered_mapunordered_set 的处境一模一样,只不过它存储的是键值对 。它和 map 的差异也完全可以套用上面那三条:

  1. 对键的要求map 需要 operator<unordered_map 需要哈希和相等比较。

  2. 迭代器与遍历map 双向,遍历时按键的升序输出;unordered_map 单向,遍历时无序。

  3. 性能 :多数情况下 unordered_map 的增删查是 O(1),比 map 的 O(log N) 快。

接口上,unordered_mapmap 几乎一致,常用的 inserterasefind,以及非常好用的 operator[] 都完全支持。比如:

cpp

复制代码
unordered_map<string, int> umap;
umap["apple"] = 5;
umap["banana"] = 3;
cout << umap["apple"] << endl;  // 输出 5

用起来和 map 没有差别,只是迭代时无法得到排序效果而已。


4. 允许重复的版本:unordered_multiset 与 unordered_multimap

和有序容器家族一样,无序容器也有允许键值重复的版本:

  • unordered_multiset:元素可重复,接口与 multiset 类似。

  • unordered_multimap:键可以重复,但不支持 operator[](因为同一个键可能对应多个值,这个操作就模糊了)。

当你需要保留重复元素、又想获得哈希表的高效查找时,就用这两个容器。


5. 一些和哈希实现相关的接口

unordered_set / unordered_map 还提供了一些和哈希表内部结构相关的接口,主要分两类:

  • Buckets 系列bucket_count()(当前桶的数量)、bucket_size(n)(第 n 个桶中的元素个数)、bucket(key)(键落在哪个桶)等。

  • Hash policy 系列load_factor()(负载因子)、max_load_factor()(最大负载因子)、rehash(n)(重设桶数量)、reserve(n)(提前预留空间以达到某个容量)。

日常写业务代码时很少需要手动操控这些接口,默认行为已经能应对绝大多数场景。等你深入学习哈希表的底层实现,再回头看它们就会一目了然:无非就是调整桶数和负载因子,平衡空间和查找效率。


6. 小结

简单总结一下选择策略:

  • 如果你需要元素/键按顺序排列 ,或者需要双向迭代器 ,选择 setmap

  • 如果你追求增删查性能 ,不在意元素顺序,优先用 unordered_set / unordered_map

  • 如果键允许重复,把后缀换成 multiset / multimapunordered_multiset / unordered_multimap

  • 记得:有序容器依赖 <,无序容器依赖哈希函数==。自定义类型当作键时,务必提供哈希函数和相等比较。

希望这篇文章能帮你理清无序容器的使用场景和注意事项,不至于在 setunordered_set 之间犹豫不决。

相关推荐
洛水水1 分钟前
【数据结构】红黑树详解
数据结构·红黑树
炸膛坦客1 分钟前
嵌入式 - 数据结构与算法:(1-9)数据结构 - 队列(Queue)
c语言·数据结构
lbb 小魔仙12 分钟前
基于Python构建RAG(检索增强生成)系统:从原理到企业级实战
开发语言·python
~|Bernard|28 分钟前
二.go语言中map的底层原理(2026-5-8)
算法·golang·哈希算法
代码的小搬运工33 分钟前
UITableView
开发语言·ui·ios·objective-c
刚子编程35 分钟前
C# Join 深度解析:参数顺序、多表关联与空值处理最佳实践
开发语言·c#·最佳实践·join·多表关联·空值处理
AbandonForce36 分钟前
哈希表(HashTable,散列表)个人理解
开发语言·数据结构·c++·散列表
代码中介商42 分钟前
栈结构完全指南:顺序栈实现精讲
c语言·开发语言·数据结构
平凡但不平庸的码农1 小时前
Go 错误处理详解
开发语言·后端·golang
样例过了就是过了1 小时前
LeetCode热题100 编辑距离
数据结构·c++·算法·leetcode·动态规划