【C++ STL篇(十三)】无序关联容器 unordered_set / unordered_map解析

C++ STL篇(十三) ------ unordered_set / unordered_map

前言:为什么我们需要"无序"?

  在学习 C++ 的过程中,我们首先接触的关联式容器通常是 setmap。它们底层由红黑树 实现,能够自动将元素按照 key 的大小排序,遍历时得到一个有序序列,增删查改的时间复杂度稳定在 O(log N)。既然它又有序又稳定,为什么还要引入 unordered_setunordered_map 呢?

  答案藏在哈希表 里。对于大多数只需要"快速查找、快速插入、快速删除"的场景,我们并不关心元素是否有序。哈希表能够在平均 O(1) 的时间内完成这些操作,比 O(log N) 快得多。因此,C++11 引入了以哈希表为底层的无序关联容器,它们就是 unordered_setunordered_mapunordered_multisetunordered_multimap

文章目录

  • [C++ STL篇(十三) ------ unordered_set / unordered_map](#C++ STL篇(十三) —— unordered_set / unordered_map)
    • 前言:为什么我们需要"无序"?
    • [1. unordered_set 系列的使用](#1. unordered_set 系列的使用)
      • [1.1 参考文档](#1.1 参考文档)
      • [1.2 unordered_set 的声明与模板参数](#1.2 unordered_set 的声明与模板参数)
      • [1.3 与 set 的差异:三个角度彻底看懂](#1.3 与 set 的差异:三个角度彻底看懂)
      • [1.4 使用接口对比](#1.4 使用接口对比)
    • [2. unordered_map 的使用差异](#2. unordered_map 的使用差异)
      • [2.1 与 map 的三个核心差异](#2.1 与 map 的三个核心差异)
      • [2.2 使用接口对比](#2.2 使用接口对比)
    • [3. unordered_multiset 与 unordered_multimap](#3. unordered_multiset 与 unordered_multimap)
    • [4. 哈希相关接口:桶与负载因子](#4. 哈希相关接口:桶与负载因子)
      • [4.1 Buckets(桶)相关](#4.1 Buckets(桶)相关)
      • [4.2 Hash policy(哈希策略)](#4.2 Hash policy(哈希策略))
    • 结语:

1. unordered_set 系列的使用

1.1 参考文档

传送门:cplusplus------unordered_set

1.2 unordered_set 的声明与模板参数

先来看 unordered_set 的完整声明:

虽然这四个模板参数看着有点吓人,但大部分时候我们只需要传第一个。

这里我们来拆解一下:

  • Key :你想存储的元素类型,也就是关键字。在 unordered_set 中,key 就是 value,因为集合里只存键,没有映射值。
  • Hash = hash :把 Key 变成整数 的仿函数。哈希表需要用一个无符号整数来定位元素该放在哪个桶里,因此要求 Key 能转换为整数 。标准库已经为 intstring、指针等常用类型实现了 hash,如果你用自定义类型,就需要自己写一个仿函数,让它可以计算出哈希值。
  • Pred = equal_to :判断两个 Key 是否相等 的仿函数。哈希表定位到桶之后,还要在桶内逐个比较元素才能确认是同一个 key。默认用 equal_to<Key>,也就是调用 operator==
  • Alloc = allocator:空间配置器,负责内存的申请与释放。一般不用管,除非你想做内存池优化。

1.3 与 set 的差异:三个角度彻底看懂

差异一:对 Key 的要求截然不同
  • set 要求 Key 支持小于比较 (operator<),因为它底层红黑树要靠比较来维持有序。
  • unordered_set 要求 Key 支持转换成整数 (哈希) 和相等比较 (operator==),这是哈希表底层原理决定的。
差异二:迭代器的区别
  • 迭代器类型set::iterator双向 迭代器,可以 ++--unordered_set::iterator单向 迭代器,只能 ++。因为哈希桶通常是用单链表串起来的,不支持逆向遍历。
  • 遍历顺序set 遍历是中序遍历,元素按 key 升序 输出,自带去重功能。unordered_set 遍历没有任何顺序保证,它取决于哈希值和桶的分布,输出是无序的,但也去重。
差异三:性能上的实质差异
  • 红黑树的增删查都是 O(log N)。
  • 哈希表的平均 复杂度是 O(1),最坏情况下退化为 O(N)(当所有元素挤在同一个桶里时)。
    实际使用中,通过良好的哈希函数和控制负载因子 (元素个数 / 桶个数),我们能保证它接近 O(1)。因此,在绝大部分场景下,unordered_set 的增删查改比 set 要快很多。

下面用一段代码来直观感受这些差异。

cpp 复制代码
void test_set1()
{
    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());      // N比较大时重复值较多
        v.push_back(rand() + i);    // 重复值较少
       // v.push_back(i);          // 无重复,有序
    }

    // 1. 插入测试
    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;

    // 实际插入的元素个数
    cout << "插入数据个数(set): " << s.size() << endl;
    cout << "插入数据个数(unordered_set): " << us.size() << endl << endl;

    // 2. 查找测试
    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;

    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: " << end4 - begin4 << " -> " << m2 << endl << endl;

    // 3. 删除测试
    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 << endl;
}

效果:

运行结果(你的环境可能不同):

  • set insert 耗时可能是 unordered_set insert 的两到三倍。
  • 查找和删除也有类似差距,unordered_set 明显更快。

1.4 使用接口对比


2. unordered_map 的使用差异

参考文档

传送门:cplusplus------unordered_map

  unordered_mapunordered_set 几乎一样,唯一的区别是:它存储的是键值对 pair<const Key, T> 。所以它的 Key 要求、迭代器性质、性能特征与 unordered_set 完全一致,只是多了一个映射值 mapped_type

2.1 与 map 的三个核心差异

  • Key 的要求map 要求 Key 支持 < 比较;unordered_map 要求 Key 支持哈希转换和相等比较。
  • 迭代器map 是双向迭代器,遍历 Key 升序unordered_map 是单向迭代器,遍历 无序
  • 性能 :大多数情况下 unordered_map 增删查更快,尽管复杂度为平均 O(1),但哈希表常数略大,若数据量很小,红黑树可能反而显得更快。一般当 N > 几百时,哈希优势开始显现。

2.2 使用接口对比


3. unordered_multiset 与 unordered_multimap

  • 允许键重复 :与 multiset/multimap 类似,这两个容器允许插入重复的键。
  • 差异依然是三点 :对 key 的要求(哈希+判等)、迭代器单向遍历无序、性能平均 O(1)。
  • 使用时注意 insert 总是成功,返回一个迭代器(不是 pair),因为不存在重复导致的失败。
  • 统计某个键的个数用 count(key),它会返回该键出现的次数。

4. 哈希相关接口:桶与负载因子

  unordered_setunordered_map 提供了许多与哈希表底层相关的接口,虽然平常不常用,但了解它们有助于理解性能调优。这些接口在文档中分为 BucketsHash policy 两组。

4.1 Buckets(桶)相关

  • bucket_count():返回当前桶的总数
  • max_bucket_count():系统允许的最大桶数。
  • bucket_size(n):第 n 个桶中元素的个数。
  • bucket(const key_type& k):返回键 k 会被放在哪个桶(桶索引)。

  这些接口可以帮助你分析哈希分布是否均匀。例如遍历所有桶,统计每个桶的大小,如果发现个别桶特别长,说明哈希函数不够均匀,或者负载因子太高。

4.2 Hash policy(哈希策略)

  • load_factor():当前负载因子,计算公式为 size() / bucket_count()
  • max_load_factor():获取或设置最大负载因子 。当 load_factor() 超过该值时,容器会自动rehash,增加桶的数量并重新分配所有元素。
  • rehash(count):手动触发 rehash,将桶数调整为至少 count
  • reserve(n):为至少容纳 n 个元素预留桶。

结语:

  因为之前已经详细讲解过 map和set 所以本文对一些重复度较高的部分就没有过多赘述,有需要的小伙伴可以看看博主往期的文章。꜀(˘꒳˘ ꜀)

【C++ STL篇(八)】set容器------零基础入门与核心用法精讲
【C++ STL篇(九)】map容器------零基础入门与核心用法精讲
【C++ STL篇(十二)】红黑树の影分身:一棵树如何同时化身 map 和 set(万字拆解+源码)

  今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

相关推荐
白日与明月2 小时前
pip下载库指定操作系统及python版本
开发语言·python·pip
折哥的程序人生 · 物流技术专研2 小时前
Qoder 1.0 完全指南:从安装到Agents驱动开发实战
开发语言·人工智能·python·ai编程
Xin_ye100862 小时前
C# 零基础到精通教程 - 第十六章:ASP.NET Core Web API——构建现代 Web 服务
开发语言·c#
basketball6162 小时前
Go语言介绍
开发语言·go
霸道流氓气质2 小时前
Spring Data JPA 完全指南
开发语言·数据库
Mortalbreeze2 小时前
C++11 ---- 列表初始化
c++
dualven_in_csdn2 小时前
cmd切换到powershell (一)
服务器·开发语言·php
会编程的土豆2 小时前
Go 里的 init() 到底是什么(彻底理解)
开发语言·后端·golang
PAK向日葵2 小时前
【C++】深入浅出,理解 C++ 奇异递归模板模式(CRTP)
c++·后端·面试