C++ 中 unordered_set 与 unordered_map 有什么不同?一篇讲透

在 C++ 标准库中,std::unordered_set 和 std::unordered_map 是使用频率极高的两个关联容器。它们都基于哈希表实现,都提供平均 O(1) 的查找性能,名字也长得很像,因此经常被初学者混用或混淆。本文从底层结构、存储内容、接口差异、性能特征和选型建议几个角度,把两者的区别讲清楚。

一、一句话概括区别

unordered_set 存的是"键本身",unordered_map 存的是"键到值的映射"。

换句话说,unordered_set 只关心"某个元素在不在集合里",而 unordered_map 关心"某个键对应什么值"。前者是集合(Set),后者是字典/映射(Map)。

二、底层结构:同为哈希表,但节点内容不同

两者在标准中都没有规定具体实现,但主流实现(libstdc++、libc++、MSVC STL)都采用**链地址法(separate chaining)**的哈希表:一个桶数组,每个桶挂一条链表(或红黑树,见下文)。

真正的差异在于节点里存什么:

  • unordered_set<Key> 的节点只存一个 Key,哈希函数作用于 Key,相等比较也作用于 Key。
  • unordered_map<Key, T> 的节点存一个 std::pair<const Key, T>,哈希和相等比较只作用于 Key,T 只是被"携带"的值。

因此从内存角度看,unordered_map 每个节点比 unordered_set 多出一个 T 的空间(以及可能的对齐填充)。当 T 很大时,这个差距会非常明显。

三、模板参数与类型定义对比

先看两者的声明形式:

cpp 复制代码
template<
    class Key,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator<Key>
> class unordered_set;

template<
    class Key,
    class T,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator<std::pair<const Key, T>>
> class unordered_map;

可以看到 unordered_map 多了一个 T 参数,分配器分配的元素类型也从 Key 变成了 std::pair<const Key, T>。

对应的 value_type 差异是理解两者接口的关键:

项目 unordered_set<Key> unordered_map<Key, T>
value_type Key std::pair<const Key, T>
key_type Key Key
mapped_type 不存在 T
迭代器解引用得到 const Key& pair<const Key, T>&

注意 unordered_set 的迭代器解引用得到的是 const Key&,因为修改键会破坏哈希表结构;而 unordered_map 的迭代器允许修改 second(值),但 first(键)同样是 const 的。

四、核心接口差异

4.1 插入方式

unordered_set 直接插入元素本身:

cpp 复制代码
std::unordered_set<std::string> words;
words.insert("hello");
words.emplace("world");

unordered_map 插入的是键值对,常用 operator[]、insert、emplace、try_emplace:

cpp 复制代码
std::unordered_map<std::string, int> counter;
counter["hello"] = 1;                       // 不存在则默认构造 int(0) 再赋值
counter.insert({"world", 2});               // 已存在则不覆盖
counter.emplace("cpp", 3);                  // 原地构造
counter.try_emplace("rust", 4);             // C++17,键已存在时不构造 value

这里有一个经典陷阱:operator[] 在键不存在时会默认构造 一个 T 并插入,因此它不能用于 const map,也可能意外插入元素。只想查询时应该用 find 或 at。

4.2 查找与访问

两者都有 find、count、contains(C++20):

cpp 复制代码
if (words.contains("hello")) { /* ... */ }

auto it = counter.find("hello");
if (it != counter.end()) {
    std::cout << it->second << '\n';   // map 通过 second 取值
}

区别在于:unordered_set 找到后拿到的就是元素本身;unordered_map 找到后拿到的是 pair,需要通过 it->second 访问值。此外 unordered_map 独有 operator[] 和 at() 两个按键访问值的接口。

4.3 删除

两者都支持按迭代器、按键、按范围删除,接口基本一致:

cpp 复制代码
words.erase("hello");
counter.erase("world");

五、完整示例对比

下面用同一个需求------统计一段文本中每个单词出现的次数,并判断某个单词是否出现过------来对比两种写法。

cpp 复制代码
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <unordered_set>

int main() {
    std::string text = "the quick brown fox jumps over the lazy dog the fox";
    std::istringstream iss(text);
    std::string word;

    // unordered_map:统计词频
    std::unordered_map<std::string, int> freq;
    while (iss >> word) {
        ++freq[word];
    }

    for (const auto& [w, c] : freq) {
        std::cout << w << " : " < c << '\n';
    }

    // unordered_set:只关心去重后的单词集合
    std::unordered_set<std::string> unique_words;
    std::istringstream iss2(text);
    while (iss2 >> word) {
        unique_words.insert(word);
    }

    std::cout << "unique count = " < unique_words.size() << '\n';
    std::cout << "has fox? " << (unique_words.count("fox") ? "yes" : "no") << '\n';

    return 0;
}

可以看到:需要"值"的时候用 unordered_map,只需要"存在性"的时候用 unordered_set,后者更省内存、语义也更清晰。

六、性能与内存对比

维度 unordered_set unordered_map
平均查找/插入/删除 O(1) O(1)
最坏情况 O(n) O(n)
单节点内存 Key + 指针/控制信息 Key + T + 指针/控制信息
迭代顺序 无序 无序
迭代器失效 rehash 时全部失效,删除仅影响被删元素 同左

几点值得注意:

  • 最坏情况都是 O(n) :当所有键哈希到同一个桶时,链表退化为线性查找。C++11 起标准要求 rehash 后仍保持平均 O(1),但恶意构造的哈希碰撞仍可能拖慢性能。
  • rehash 会失效迭代器 :插入导致负载因子超过 max_load_factor 时会触发 rehash,所有迭代器失效,但引用和指针仍然有效(因为节点本身没有移动)。
  • 内存开销 :两者每个元素都有额外的指针开销,元素数量很少时反而不如 std::vector 或 std::map 划算。

七、如何选择

  • 只需要判断元素是否存在、去重 :用 unordered_set。
  • 需要键到值的映射、计数、缓存、索引 :用 unordered_map。
  • 需要有序遍历或范围查询 :两者都不合适,应改用 std::set / std::map(基于红黑树,O(log n))。
  • 键的类型没有现成的 std::hash 特化:需要自己提供哈希函数,两者都要做同样的工作。
  • 对内存极度敏感 :优先考虑 unordered_set,或使用开源的扁平哈希表(如 absl::flat_hash_map)。

八、常见误区

  1. 误以为 unordered_map 可以用迭代器修改键 :it->first 是 const Key,编译期就会报错。
  2. 用 operator[] 做只读查询 :会意外插入默认值,应改用 find / at / contains。
  3. 认为两者性能完全一样 :unordered_map 节点更大,缓存局部性更差,在元素多、值类型大时差距明显。
  4. 依赖遍历顺序:两者都是无序容器,遍历顺序取决于哈希函数和桶数量,不同标准库实现、不同版本都可能不同。

九、小结

unordered_set 和 unordered_map 共享同一套哈希表设计思想,核心区别只有一条:是否携带映射值 。理解这一点后,模板参数、value_type、迭代器解引用类型、operator[] 的有无、内存开销的差异,就都能顺理成章地推导出来。实际编码时,先问自己"我只需要知道在不在,还是需要拿到对应的值",答案自然就出来了。

相关推荐
外收内放1 小时前
FastAPI猜字谜实战(6):日志与异常
开发语言·python·学习·fastapi
垆边人似月.1 小时前
预处理与参数调优(让识别率从“能看”到“可用”)
开发语言·python·自然语言处理
泡泡糖的中文规格书1 小时前
HDLbits 2.2 学习笔记 · 多路复用器
java·开发语言
John_Hui1 小时前
第 3 篇:配置 JDK 17 环境
java·开发语言·ubuntu
鶴哥只手遮天2 小时前
AFSIM 2.9.0 深度解析:架构、原理与工程实践-第 1 章 项目全景与代码地图
c++
充值内卷2 小时前
C++单文件迷宫游戏:TauriCPP 开发全记录
开发语言·c++·游戏
Antruly2 小时前
C++ Web 开发选型漫谈:从 Drogon 到 libuvcpp + uvp,八个项目的定位、兼容性与务实排名
开发语言·前端·c++
EnCi Zheng2 小时前
AI Agent工具调用防护层怎么写-一份能直接跑的Python四层拦截实现
大数据·开发语言·python
香蕉大香蕉(06敬畏市场)2 小时前
高频八股 · Java 并发编程(10):异步、ThreadLocal 与虚拟线程(收官)
java·开发语言·网络·jvm·python·ai·实习