简单聊聊 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 之间犹豫不决。

相关推荐
华科大胡子2 分钟前
AI开发者的网络卡点:Anthropic连接超时
开发语言·php
磊 子25 分钟前
STL无序关联容器—unorded_set+unorded_map
开发语言·c++
初夏睡觉34 分钟前
数据结构学习之~二叉堆 (P3378 【模版】堆)
数据结构·c++·学习
AI人工智能+电脑小能手37 分钟前
【大白话说Java面试题 第84题】【Mysql篇】第14题:为什么用 InnoDB 存储引擎的表建议用整型的自增主键?
java·开发语言·数据库·mysql·面试
云泽8081 小时前
笔试算法 - 链表篇(一):移除、反转、合并、回文判断全解析
数据结构·c++·算法·链表
也曾看到过繁星1 小时前
数据结构-复杂度
数据结构
菜菜的顾清寒1 小时前
HOT力扣100(43)二叉树-翻转二叉树
数据结构·算法·leetcode
小poop1 小时前
深入理解指针(中):数组与指针的进阶之旅
c++
YikNjy1 小时前
break和continue
java·开发语言·算法
秋92 小时前
java项目中cpu飙升排查及解决方法
java·开发语言