探究哈希表:高效的数据存储与快速检索

前言

在计算机科学领域,哈希表是一种重要的数据结构,被广泛应用于各种编程语言和软件系统中。它通过哈希函数将键映射到存储桶,实现了快速的插入、删除和查找操作,成为处理大规模数据集合的利器。本文将深入探讨哈希表的原理、应用,带大家领略哈希表在计算机科学中的重要作用。

一、数组与链表的缺陷

场景假设:我们需要在数组和链表中添加(尾部)、查找(任意位置)、删除(任意位置)元素 tq

workspace.png

  • 数组添加元素:将元素添加至数组的尾部,时间复杂度 𝑂(1) 。
  • 数组查询元素:需要遍历其中的所有元素,时间复杂度 𝑂(𝑛)。
  • 数组删除元素:先查询元素,再删除元素,时间复杂度 𝑂(𝑛)。

workspace (1).png

链表存在头尾指针:

  • 链表添加元素:将元素添加至链表的尾部,时间复杂度 𝑂(1) 。
  • 链表查询元素:需要遍历其中的所有元素,时间复杂度 𝑂(𝑛)。
  • 链表删除元素:先查询元素,再删除元素,时间复杂度 𝑂(𝑛)。

数组和链表的对比效果:

数组链表
查找元素𝑂(𝑛)𝑂(𝑛)
添加元素𝑂(1)𝑂(1)
删除元素𝑂(𝑛)𝑂(𝑛)

我们通过上表可以看出:数组与链表对于添加元素(尾部添加)的效率还是可以的,但是针对查找和删除操作时可能就不太理想了。其实,删除之所以效果不太理想就是因为在这之前有查询的操作。

我们可以发现其本质是:无论是数组还是链表针对查询都需要遍历,从而影响了性能

二、哈希表

针对数组和链表的问题,是否能够设计一种数据结构做到:不需要遍历便能定位元素

我们可以考虑如下思路:

  1. 使用键值对存储元素,键存储一个索引,值存储需要存储的元素
  2. 定义一个映射函数,这个函数的输入值是要查询的元素,输出结果是一个索引

当要查询元素时,先通过映射函数进行一次计算获取到对应的索引,然后根据索引获取到相应的元素。这样处理之后,我们可以在时间复杂度为 𝑂(1) 的情况完成添加、查找、删除等操作。

workspace (6).png

上述的思路其实有一个专门的名词——哈希表。哈希表是一种基于哈希函数实现的数据结构,用于存储键值对(key-value pairs)。它通过哈希函数将键映射到数组中的特定位置,这个位置通常称为哈希桶。哈希函数将键转换成索引,使得数据可以快速地插入、查找和删除。哈希表的设计使得平均情况下这些操作的时间复杂度为 𝑂(1) ,使其成为处理大量数据时高效的数据结构。

哈希表的历史可以追溯到 20 世纪 50 年代和 60 年代的计算机科学研究中。

  1. 早期研究阶段: 哈希表的概念最早可以追溯到 20 世纪 50 年代和 60 年代,当时的计算机科学家开始探索如何提高数据的检索效率。这一阶段主要是对哈希函数和相关技术进行研究,用于信息编码、压缩等领域。
  2. 理论研究阶段: 哈希表的理论基础在 20 世纪 60 年代得到了发展,Donald Knuth 等人的工作对理解哈希表的性能和实现起到了重要作用。他们研究了不同类型的哈希函数和解决冲突的方法,为实际应用提供了重要的指导。
  3. 实际应用阶段: 哈希表在 20 世纪 70 年代和 80 年代得到了广泛的实际应用,尤其是在编程语言和数据库系统中。许多编程语言和标准库都提供了哈希表的实现,如 C 语言的哈希表、Python 的字典等。这一阶段的发展主要集中在如何将理论研究成果应用到实际系统中,并优化其性能和稳定性。
  4. 性能优化阶段: 随着计算机硬件性能的提升和算法优化的发展,对哈希表的性能要求也越来越高。因此,人们对哈希函数的设计和解决冲突的算法进行了持续的研究和优化,以提高哈希表的效率和稳定性。这一阶段的发展主要集中在如何进一步优化哈希表的性能,以应对不断增长的数据量和复杂的应用场景。

三、哈希冲突

哈希表是一种非常优秀的数据结构,但是也存在一些问题。其中最重要的问题之一就是哈希冲突

workspace (1).png

哈希冲突是指两个或多个不同的键被哈希函数映射到了同一个存储桶的情况。在哈希表中,由于存储桶的数量是有限的,而键的数量是可能无限的,因此哈希冲突是无法避免的。例如:tqxx 通过哈希函数计算之后都映射到了同一个哈希槽位。

哈希冲突如果发生了,就会导致无法正确处理值(包括查询、添加、删除)。

解决哈希冲突一般有两种思路:

  1. 扩容哈希表,哈希表的容量大了发生哈希冲突的概率就小了(事前预防)
  2. 针对哈希表进行改良优化(事后处理)

第一种思路:扩容哈希表,虽然容量大了,可以减小哈希冲突的概率,但是这并不意味着解决了哈希冲突。并且,扩容操作是会影响性能的。

第二种思路:是一种事后处理的解决方案,通常有 “链式地址” 和 “开放寻址” 两种方案。

以上两种思路各有优缺点,而一般我们是同时采用上述两种方式来处理哈希冲突的。即:改良优化哈希表 + 扩容哈希表。

3.1 链式地址

workspace (1).png

链式地址,也称为链式哈希或链式散列,是一种解决哈希冲突的方法之一。在链式地址中,哈希表的每个存储桶不仅可以存储一个键值对,而是可以存储多个键值对,这些键值对通过链表或其他形式的链式结构连接在一起。
链式地址虽然解决了哈希冲突,却依然存在一个比较大的问题。

试想一下,倘若链表的长度非常长,会发生什么?

哈希表查询的时间复杂度将会退化到 𝑂(𝑛)。

此时的处理方案一般是将链表转换成红黑树或 “AVL 树”

3.2 开放寻址

链式地址额外引入了链表结构,而开放寻址的思路是不引入额外数据结构来解决。其主要思路是通过多次探测来解决哈希冲突。常见的探测方式有以下几种:

  1. 线性探测
  2. 平方探测
  3. 多次哈希

3.2.1 线性探测

线性探测采用固定步长的线性搜索来进行探测。以插入元素为例:

  1. 通过哈希函数计算哈希值
  2. 如果发生哈希冲突,则从冲突位置向后进行线性遍历(通常遍历的步长为 1 1 1
  3. 遍历过程,找到了空位置,则插入元素

workspace.png

线性探测有一个比较大的问题:如果步长为 1 1 1 ,很容易导致位置被连续占用从而形成聚集现象,而连续位置发生哈希冲突的概率又比较高,又会进一步加重聚集现象,从而形成恶性循环。

workspace (1).png

3.2.2 平方探测

在线性探测中,是以固定的步长进行线性遍历。而平方探测与线性探测是类似的,不同的是平方探测不是固定步长而是跳过 “探测次数的平方” 例如:跳过 1 2 , 2 2 , 3 2 . . . 1^2,2^2,3^2... 12,22,32...

workspace (2).png

平方探测会使数据分布更加均匀,缓解了线性探测的聚集现象。需要注意的是:平方探测仅是在一定程度缓解了聚集现象并不是解决了聚集现象。

3.2.3 多次哈希

多次哈希是指对数据进行多次哈希运算的过程。多次哈希可以增加哈希过程的复杂度,减少哈希冲突的可能性。

workspace.png

多次哈希不易产生聚集现成,但是会增加哈希计算的复杂度。

四、哈希函数

workspace.png

倘若,我们以数组方式实现了一个哈希表,数组索引的计算逻辑如下:

i n d e x = h a s h ( k e y ) % c a p a c i t y index = hash(key) \% capacity index=hash(key)%capacity

  1. 首先,通过 hash(key) 计算哈希值
  2. 最后,通过哈希值对数组 capacity 取模,得到数组索引

我们可以发现:真正决定哈希位置的是哈希函数,即 hash(key)(例如: h a s h ( t q ) = 4 hash(tq)=4 hash(tq)=4 h a s h ( x x ) = 4 hash(xx)=4 hash(xx)=4)。由此可见,哈希函数的设计是十分重要的。

而哈希函数的设计有许多问题需要解决:

  1. 唯一性和一致性:哈希函数应该将不同的输入数据映射到不同的哈希值,确保哈希值的唯一性。同时,对于相同的输入数据,哈希函数应该始终生成相同的哈希值,确保哈希值的一致性。
  2. 高效性:哈希函数的计算过程应该快速高效,能够在较短的时间内生成哈希值。
  3. 分散性:良好的哈希函数应该能够将输入数据均匀地分散到哈希空间中,减少哈希冲突的可能性。
  4. 抗碰撞性:哈希函数应该具有一定程度的抗碰撞性,即不同的输入数据产生相同的哈希值的可能性很小。这有助于保护数据的完整性和安全性。
  5. 不可逆性:哈希函数应该是不可逆的,即无法通过已知的哈希值来还原出原始的输入数据。这有助于保护数据的隐私和安全。
  6. 输入数据长度不限制:哈希函数应该能够处理任意长度的输入数据,并生成固定长度的哈希值。

哈希函数的核心是哈希算法,要设计一个算法满足上述的问题还是比较复杂的。比较幸运的是市面上已经有一些常见的哈希算法:

MD5SHA-1SHA-2SHA-3
推出时间(年份)1992199520022008
输出长度(bit)128160256/512224/256/384/512
哈希冲突较多较多很少很少
安全等级
应用弃用(但可用于数据完整性检查)弃用加密货币交易验证、数字签名等新的哈希算法标准

五、哈希表扩容

之前,我们知道一个好的哈希算法能够降低哈希冲突的概率。可是,就算哈希算法再好,如果哈希表的容量小也是没用的。

一个极端的例子是:如果哈希表的容量是 1 1 1,那么所有结果都将会指向唯一的哈希槽。

workspace.png

哈希表的容量比较小,很容易就会发生哈希冲突。因此,合理的哈希表容量搭配优秀的哈希算法才能满足我们的需求。可是,哈希表创建好之后,由于不断使用,可用的哈希槽在不断减少,哈希表的可用容量与哈希算法的适配效果将会逐步下降,从而将会增加哈希冲突的概率。

之前,我们可以通过 “链式地址” 和 “开放寻址” 等方法来解决哈希冲突。可是这种解决哈希冲突的方式是一种事后的处理方式,属于治标不治本。

如果我们想降低当前哈希表发生哈希冲突的概率,通常我们可以通过扩容的方式来实现。

workspace (2).png

扩容可以降低哈希冲突概率,可随之而来有两个问题:

  1. 扩容的时机是什么?(即:应该在什么时候才扩容)
  2. 扩容的大小?

针对第一个问题:我们引入一个负载因子(load factor)的概念。
负载因子:哈希表的元素数量除以桶数量(例如: 5 / 10 = 0.5 5 / 10 = 0.5 5/10=0.5),用于衡量哈希冲突的严重程度,也常作为哈希表扩容的触发条件。

针对第二个问题:一般扩容的大小是多方面考虑的权衡并非有一个固定的标准。

例如:在 Java 中,当负载因子超过 0.75 0.75 0.75,哈希表将会扩容至原先的 2 2 2 倍。

六、哈希表的应用场景

哈希表作为一种高效的数据结构,在计算机科学和软件工程中有着广泛的应用。以下是一些具体的哈希表应用场景:

  1. 字典数据结构: 哈希表常被用作字典数据结构,用于存储键值对。在编程中,例如 Python 中的字典(dict)、Java 中的 HashMap 等,都是基于哈希表实现的。这种结构非常适合存储配置信息、缓存数据、路由表等。
  2. 缓存实现: 哈希表可以用于实现缓存,快速地存储和检索数据,提高系统性能。当需要频繁访问的数据可以通过哈希表进行快速查找时,可以将这些数据缓存起来,减少对底层存储系统的访问次数,从而提高系统的响应速度。
  3. 数据索引: 数据库系统中的哈希索引可以通过哈希表来实现,加速数据检索操作。哈希索引通常适用于等值查询,即根据某个列的值来查找对应的行。当数据库表中的数据量较大时,使用哈希索引可以快速定位到需要查询的数据行,提高查询效率。
  4. 唯一性检查: 哈希表可以用于检查一组数据中是否存在重复的元素。通过将元素映射到哈希表中,并检查每个元素是否已经存在,可以快速地判断是否有重复元素。这在数据去重、数据校验等场景中非常有用。
  5. 分布式系统中的一致性哈希: 一致性哈希算法利用哈希表来实现分布式系统中的负载均衡和数据分片。通过哈希函数将数据映射到一组节点(服务器)上,可以保证数据均匀分布,并且在节点的增加或减少时能够最小化数据迁移的成本。
  6. 密码学: 哈希表在密码学中也有着重要的应用,比如存储用户密码的哈希表,可以确保用户密码的安全性。通常情况下,密码不会直接存储在数据库中,而是存储其哈希值,这样即使数据库泄露,黑客也无法直接获取用户的明文密码。
  7. 编译器和解释器中的符号表: 哈希表常被用于编译器和解释器中的符号表(Symbol Table)实现,用于存储变量名、函数名等标识符,并快速地进行查找和更新操作。符号表在编译和解释过程中起到了重要的作用,它记录了程序中所有的标识符及其相关信息,如类型、作用域等。

七、小结

哈希表作为一种高效的数据结构,为我们处理大规模数据提供了重要的工具。通过合理地利用哈希表,我们可以解决各种问题,提高系统的性能和可扩展性。

推荐阅读

  1. Spring 三级缓存
  2. 深入了解 MyBatis 插件:定制化你的持久层框架
  3. Zookeeper 注册中心:单机部署
  4. 【JavaScript】探索 JavaScript 中的解构赋值
  5. 深入理解 JavaScript 中的 Promise、async 和 await
  • 48
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值