【数据结构】`unordered_map` 和 `unordered_set` 的底层原理

news2024/11/19 13:16:19

unordered_mapunordered_set 是 C++ 标准库中的两个容器,它们被广泛应用于需要快速查找的场景中。它们的查找、插入和删除的平均时间复杂度都是 O(1),这也是它们的一个重要特性。本文将详细介绍 unordered_mapunordered_set 的底层原理,帮助计算机专业的小白理解什么是哈希、桶以及为什么它们的查找效率如此之高。

本篇文章需要有unordered_map、unordered_set、vector等的基础,若不清楚,建议先去了解后再来阅读

  • 【编程语言】在C++中使用map与unordered_map
  • 【编程语言】C++ 新手指南:如何使用 set 和 unordered_set
  • 【编程语言】C++ 中 vector 的常用操作方法
  • 【数据结构】链表详解:数据节点的链接原理
  • 【数据结构】时间复杂度和空间复杂度是什么?

全文共计2600字,耗时5天缝缝补补写完

若本文对你有帮助的话,可以给我点点关注和赞👍,感谢。


一、什么是 unordered_mapunordered_set

在 C++ 中,unordered_mapunordered_set 是两个基于 哈希表 实现的容器。

  • unordered_map:是一个关联容器,用于存储键值对(key-value pairs)。每个键(key)是唯一的,并且与一个值(value)相关联。
  • unordered_set:是一个无序的集合,用于存储唯一的键值(key)。它不存储重复元素。

简单代码示例

#include <iostream>
#include <unordered_map>
#include <unordered_set>

int main() {
    // 使用 unordered_map
    std::unordered_map<int, std::string> map;
    map[1] = "One";
    map[2] = "Two";

    // 使用 unordered_set
    std::unordered_set<int> set;
    set.insert(1);
    set.insert(2);

    // 输出 map 和 set 内容
    std::cout << "Map:" << std::endl;
    for (const auto& pair : map) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    std::cout << "Set:" << std::endl;
    for (const auto& item : set) {
        std::cout << item << std::endl;
    }

    return 0;
}

二、哈希表的工作原理

哈希表的关键概念包括桶(bucket)、哈希函数(hash function)、链表等。我们将通过一些符号表示出其内部结构。

在标准库的实现中,哈希表的桶通常是一个 std::vector,但其具体实现可能因标准库(如 libstdc++libc++)的不同而有所差异。下面是对桶的具体结构的详细分析:


1. 桶的结构

是一个容器,用于存储所有映射到该桶的元素。其底层实现通常是以下两种方式之一:

(1) 指针形式

每个桶存储一个指向链表(或其他结构)的指针。例如:

[ 桶0 ] -> nullptr
[ 桶1 ] -> head -> [key2] -> [key1] -> nullptr
[ 桶2 ] -> nullptr
[ 桶3 ] -> head -> [key3] -> nullptr

这种实现中,桶本身存储指向链表的指针,而链表存储具体的键值。


(2) 动态数组形式(std::vector 或类似结构)

每个桶本身是一个动态数组(如 std::vector),用于直接存储冲突的元素:

[ 桶0 ] -> 空
[ 桶1 ] -> [key1, key2]
[ 桶2 ] -> 空
[ 桶3 ] -> [key3]

2. 实际实现中的常见选择

大多数标准库实现中,unordered_mapunordered_set 的桶是用 std::vector 作为底层数据结构,并且每个桶存储一个 指向链表头部的指针,而链表用来存储具体元素。原因如下:

(1) 使用 std::vector 管理桶
  • std::vector 是常见的选择,因为它提供高效的随机访问(通过下标快速定位桶)。
  • 动态扩展的特性允许哈希表在需要时进行 rehashing(增加桶数量)。
(2) 使用链表存储冲突元素
  • 链表 允许高效插入和删除元素,尤其在发生哈希冲突时。
  • 每个链表节点可能存储键值对(对于 unordered_map)或单独的键(对于 unordered_set),并且包含指向下一个节点的指针。

3. 哈希表中桶的管理

哈希表维护一个桶数组,这个数组的大小会根据装载因子(load factor)动态调整:

  • 装载因子 是表中元素总数与桶数量的比值。例如:
    • 如果有 10 个桶和 30 个元素,装载因子为 3。
  • rehashing 发生时,桶的数量通常会增大到一个质数(比如 2 倍+1),以减少冲突概率。

rehashing 过程中:

  • 将旧桶中的元素重新分配到新桶中。
  • 由于每个桶的元素重新映射到新桶,链表也会被重新构造。

4. 小结

  • 桶本身通常是一个 std::vector,它存储指向链表的指针。
  • 链表 是冲突处理的主要数据结构,用于存储同一桶中的多个元素。
  • 这种设计兼顾了随机访问的效率(通过 std::vector)和动态操作的灵活性(通过链表)。

三、 哈希表的基本结构

哈希表中的每一个桶都是一个存放数据的单元,用于存放一个或多个元素。当我们向哈希表中插入一个键(key)时,哈希表会先通过 哈希函数 将该键映射到一个特定的桶。如下图所示:

假设有一个简单的哈希表结构,包含 5 个桶:

哈希表结构:
[ 桶0 ]    [ 桶1 ]    [ 桶2 ]    [ 桶3 ]    [ 桶4 ]

1. 哈希函数的作用

哈希函数将键值转化为整数值(索引),表示对应桶的位置。例如,对于键 key1,如果哈希函数将其映射到索引 1,那么 key1 就会存储在 桶 1 中。

hash(key1) = 1,即 key1 存在 桶 1 中。

2. 哈希冲突的处理:链地址法

由于不同的键可能会映射到同一个桶中,这种现象称为 哈希冲突。在 unordered_mapunordered_set 中,通常使用 链地址法 来处理冲突。链地址法是指,每个桶中有一个链表,用于存储发生冲突的元素。每当冲突发生时,新元素会被追加到链表的末尾。

例如,假设 key1key2 都映射到 桶 1,哈希表的结构如下所示:

[ 桶0 ]      [ 桶1 ]               [ 桶2 ]       [ 桶3 ]       [ 桶4 ]
           head -> key1 -> key2

这里,桶 1 存储一个链表,其中 key1 是链表头节点,key2 是链表的下一个节点。

3. 链表结构的插入和查找

在链表结构中:

  • 插入:新元素被追加到链表末尾。
  • 查找:通过遍历链表,依次检查节点是否与目标键匹配。

具体过程如下:

插入过程:

假设我们要插入 key3,且 hash(key3) = 1(与 key1key2 冲突)。哈希表插入后的结构如下:

[ 桶0 ]      [ 桶1 ]                       [ 桶2 ]       [ 桶3 ]       [ 桶4 ]
           head -> key1 -> key2 -> key3
查找过程:

假设要查找 key2,查找步骤如下:

  1. 通过哈希函数找到桶索引:hash(key2) = 1
  2. 在桶 1 中找到对应链表,遍历链表并依次检查各节点:
    • headkey1,不匹配,继续向后找;
    • 第二个节点是 key2,匹配成功,返回结果。

四、为什么查找是 O(1)

在理想情况下,哈希表能够将数据分布在不同的桶中,每个桶中只有少量元素,查找和插入的时间复杂度接近 O(1)。我们可以通过以下符号化结构了解平均 O(1) 时间复杂度的实现条件。

1. 理想情况

假设哈希表结构如下,其中每个桶中只有一个节点:

[ 桶0 ]    [ 桶1 ]    [ 桶2 ]    [ 桶3 ]    [ 桶4 ]
  key0      key1      key2      key3      key4

这种情况下,哈希表中没有冲突,查找过程只需要哈希函数定位到特定桶即可完成,查找时间为 O(1)。

2. 发生冲突时的情况

当哈希表元素数量增加或哈希函数无法避免冲突时,多个键会映射到同一个桶。例如:

[ 桶0 ]         [ 桶1 ]                  [ 桶2 ]       [ 桶3 ]       [ 桶4 ]
              head -> key1 -> key2

此时,在桶 1 中查找 key2,需要遍历链表,这样的查找复杂度接近 O(n)。不过,在实际使用中,C++ unordered_map 会自动 扩容,将桶数量增多,从而降低冲突发生的几率,使查找平均复杂度保持在 O(1)。


五、代码示例

1. unordered_map 查找示例

#include <iostream>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> map;
    map["apple"] = 1;
    map["banana"] = 2;
    map["cherry"] = 3;

    std::string key = "banana";
    if (map.find(key) != map.end()) {
        std::cout << "Found: " << key << " => " << map[key] << std::endl;
    } else {
        std::cout << key << " not found." << std::endl;
    }

    return 0;
}

2. unordered_set 查找示例

#include <iostream>
#include <unordered_set>

int main() {
    std::unordered_set<int> set = {1, 2, 3, 4, 5};

    int key = 3;
    if (set.find(key) != set.end()) {
        std::cout << "Found: " << key << std::endl;
    } else {
        std::cout << key << " not found." << std::endl;
    }

    return 0;
}

六、扩展内容:使用哈希表时的注意事项

1. 负载因子

负载因子定义了哈希表的装填情况。过高的负载因子会导致更多冲突,进而影响性能。在 C++ 中,可以通过 max_load_factorrehash 函数管理负载因子。

2. 自定义哈希函数

C++ 提供了 std::hash 作为默认的哈希函数,但在某些情况下我们可以自定义哈希函数。

示例:自定义哈希函数

#include <iostream>
#include <unordered_map>
#include <functional>

struct Key {
    int x, y;
    bool operator==(const Key& other) const {
        return x == other.x && y == other.y;
    }
};

struct KeyHash {
    std::size_t operator()(const Key& k) const {
        return std::hash<int>()(k.x) ^ (std::hash<int>()(k.y) << 1);
    }
};

int main() {
    std::unordered_map<Key, int, KeyHash> map;
    map[{1, 2}] = 3;

    Key key = {1, 2};
    if (map.find(key) != map.end()) {
        std::cout << "Found key (1, 2) with value: " << map[key] << std::endl;
    } else {
        std::cout << "Key (1, 2) not found." << std::endl;
    }

    return 0;
}

六、总结

unordered_mapunordered_set 是 C++ 中重要的容器类型,它们基于哈希表实现,能够在平均 O(1) 的时间内完成查找、插入和删除操作。这种特性在需要高效查找的应用场景中非常有用。理解其底层的哈希表原理及冲突处理方法,是编写高性能代码的基础。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2243403.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PCB+SMT线上报价系统+PCB生产ERP系统自动化拼板模块升级

PCB生产ERP系统的智能拼版技术&#xff0c;是基于PCB前端报价系统获取到的用户或市场人员已录入系统的板子尺寸及set参数等&#xff0c;按照最优原则或利用率最大化原则自动进行计算并输出拼版样式图和板材利用率&#xff0c;提高工程人员效率&#xff0c;减少板材的浪费。覆铜…

商业物联网详细指南:优势与挑战

物联网是信息技术行业最具前景的领域之一。为什么它如此热门呢&#xff1f;原因在于全球连接性。设备可以像人群一样相互协作。正如我们所知&#xff0c;协作能显著提高生产力。 物联网对普通用户和企业都有益处。许多日常流程可以通过传感器、扫描仪、摄像头和其他设备实现自…

开源项目低代码表单设计器FcDesigner获取表单的层级结构与组件数据

在使用开源项目低代码表单设计器FcDesigner时&#xff0c;获取和理解表单的层级结构非常关键。通过getDescription和getFormDescription方法&#xff0c;您可以清晰掌握表单组件的组织结构和层次关系。这些方法为操控表单的布局和配置提供了强大的支持。 源码地址: Github | G…

【android USB 串口通信助手】stm32 源码demo 单片机与手机通信 Android studio 20241118

android 【OTG线】 接 下位机STM32【USB】 通过百度网盘分享的文件&#xff1a;USBToSerialPort.apk 链接&#xff1a;https://pan.baidu.com/s/122McdmBDUxEtYiEKFunFUg?pwd8888 提取码&#xff1a;8888 android 【OTG线】 接 【USB转TTL】 接 【串口(下位机 SMT32等)】 需…

在云服务器搭建 Docker

操作场景 本文档介绍如何在腾讯云云服务器上搭建和使用 Docker。本文适用于熟悉 Linux 操作系统&#xff0c;刚开始使用腾讯云云服务器的开发者。如需了解更多关于 Docker 相关信息&#xff0c;请参见 Docker 官方。 说明&#xff1a; Windows Subsystem for Linux&#xff…

缓存及其不一致

在实际开发过程中&#xff0c;一般都会遇到缓存&#xff0c;像本地缓存&#xff08;直接在程序里搞个map也可以&#xff0c;但是可能会随着数据的增长出现OOM&#xff0c;建议使用正经的本地缓存框架&#xff0c;因为自己实现淘汰策略啥的挺费劲的&#xff09;、分布式缓存&…

本地部署Apache Answer搭建高效的知识型社区并一键发布到公网流程

文章目录 前言1. 本地安装Docker2. 本地部署Apache Answer2.1 设置语言选择简体中文2.2 配置数据库2.3 创建配置文件2.4 填写基本信息 3. 如何使用Apache Answer3.1 后台管理3.2 提问与回答3.3 查看主页回答情况 4. 公网远程访问本地 Apache Answer4.1 内网穿透工具安装4.2 创建…

神经网络11-TFT模型的简单示例

Temporal Fusion Transformer (TFT) 是一种用于时间序列预测的深度学习模型&#xff0c;它结合了Transformer架构的优点和专门为时间序列设计的一些优化技术。TFT尤其擅长处理多变量时间序列数据&#xff0c;并且能够捕捉到长期依赖关系&#xff0c;同时通过自注意力机制有效地…

汽车资讯新动力:Spring Boot技术革新

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了汽车资讯网站的开发全过程。通过分析汽车资讯网站管理的不足&#xff0c;创建了一个计算机管理汽车资讯网站的方案。文章介绍了汽车资讯网站的系统分析部分&…

gvim添加至右键、永久修改配置、放大缩小快捷键、ctrl + c ctrl +v 直接复制粘贴、右键和还原以前版本(V)冲突

一、将 vim 添加至右键 进入安装目录找到 vim91\install.exe 管理员权限执行 Install will do for you:1 Install .bat files to use Vim at the command line:2 Overwrite C:\Windows\vim.bat3 Overwrite C:\Windows\gvim.bat4 Overwrite C:\Windows\evim.bat…

Docker部署Kafka SASL_SSL认证,并集成到Spring Boot

1&#xff0c;创建证书和密钥 需要openssl环境&#xff0c;如果是Window下&#xff0c;下载openssl Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions 还需要keytool环境&#xff0c;此环境是在jdk环境下 本案例所使用的账号密码均为&#xff1a; ka…

【进阶系列】python简单爬虫实例

python有一个很强大的功能就是爬取网页的信息&#xff0c;这里是CNBlogs 网站&#xff0c;我们将以此网站为实例&#xff0c;爬取指定个页面的大标题内容。代码如下&#xff1a; 首先是导入库&#xff1a; # 导入所需的库 import requests # 用于发送HTTP请求 from bs4 impor…

基于Java和Vue实现的上门做饭系统上门做饭软件厨师上门app

市场前景 生活节奏加快&#xff1a;在当今快节奏的社会中&#xff0c;越来越多的人因工作忙碌、时间紧张而无法亲自下厨&#xff0c;上门做饭服务恰好满足了这部分人群的需求&#xff0c;为他们提供了便捷、高效的餐饮解决方案。个性化需求增加&#xff1a;随着人们生活水平的…

CentOS 7中查找已安装JDK路径的方法

使用yum安装了jdk8&#xff0c;但是其他中间件需要配置路径的时候&#xff0c;却没办法找到&#xff0c;如何获取jdk路径&#xff1a; 一、确认服务器是否存在jdk java -version 二、查找jdk的 java 命令在哪里 which java 三、找到软链指向的地址 ls -lrt /usr/bin/java l…

分布式----Ceph部署

目录 一、存储基础 1.1 单机存储设备 1.2 单机存储的问题 1.3 商业存储解决方案 1.4 分布式存储&#xff08;软件定义的存储 SDS&#xff09; 1.5 分布式存储的类型 二、Ceph 简介 三、Ceph 优势 四、Ceph 架构 五、Ceph 核心组件 #Pool中数据保存方式支持两种类型&…

UE5 材质里面画圆锯齿严重的问题

直接这么画圆会带来锯齿&#xff0c;我们对锯齿位置进行模糊 可以用smoothstep&#xff0c;做值的平滑过渡&#xff08;虽然不是模糊&#xff0c;但是类似&#xff09;

即插即用的3D神经元注意算法!

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;编程探索专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年11月18日10点39分 神秘男子影, 秘而不宣藏。 泣意深不见, 男子自持重, 子夜独自沉。 论文连接 点击开启你的论文编制之旅…

Mac的Terminal随机主题配置

2024年8月8日 引言 对于使用Mac的朋友&#xff0c;如果你是一个程序员&#xff0c;那肯定会用到Terminal。一般来说Terminal就是一个黑框&#xff0c;但其实Terminal是有10款官方皮肤。 每个都是不一样的主题&#xff0c;颜色和字体都会有所改变。现在就有一个方法可以很平均…

《Probing the 3D Awareness of Visual Foundation Models》论文解析——单图像表面重建

一、论文简介 论文讨论了大规模预训练产生的视觉基础模型在处理任意图像时的强大能力&#xff0c;这些模型不仅能够完成训练任务&#xff0c;其中间表示还对其他视觉任务&#xff08;如检测和分割&#xff09;有用。研究者们提出了一个问题&#xff1a;这些模型是否能够表示物体…