注:本文翻译自 https://www.baeldung.com/cs/hash-tables
1 介绍
有效管理数据的技术是计算机科学的传统热点。除了存储数据之外,从存储中高效地恢复数据是另一个相关问题。
即使使用最好的算法处理某些特定的数据,如果没有优化数据管理,我们的性能也会很差。因此,恢复和向算法提供数据以及保存其输出成为性能瓶颈。
随着时间的推移,已经提出了几种保存和管理数据的技术。例如数组、链表、树和图。这些数据结构非常适合多种用途。但是,查找和恢复存储在它们中的数据的时间复杂度通常高于另一种数据结构:哈希表。
因此,本教程将探讨与散列表有关的最相关的概念。首先,我们将简要回顾一下哈希。因此,我们将学习哈希表及其工作原理。接下来,我们将看到如何解决一些关于哈希冲突的潜在问题。最后,我们将比较哈希表与其他数据结构的数据管理复杂性。
2 哈希基础
在具体学习哈希表之前,我们需要了解哈希。总而言之,哈希是接受可变长度的输入并产生固定长度的输出值的过程,称为哈希码或简称哈希。
散列函数负责将可变长度的输入转换为散列。没有标准的哈希函数。这意味着我们可以根据数据输入的一般预期特征来开发哈希函数。
下面的图片在一个高层次上描述了哈希机制是如何工作的:
值得注意的是,散列函数可以增加或减少可变长度输入的字节数。因此,如果输入大于哈希码,字节数将减少。否则,它会增加。
简而言之,散列在计算机科学中有几种应用,例如存储和验证密码、创建消息签名以及提供数据管理结构(本教程的主要主题)。
3 哈希表
哈希表是将特定键与相应值关联起来的数据结构。这些表通常使用关联数组来实现,以存储数据。此外,它们使用哈希函数来计算应该将数据存储在数组的哪一点(索引)。
因此,我们可以将哈希表理解为键值查找。因此,给定一个与值(数据)相关联的键,我们可以通过对表的快速查找来恢复相应的值。
例如,我们可以用哈希表将人们的姓名与其个人信息关联起来。这样,人们的名字就是我们的原始钥匙。哈希函数处理这些原始键以确定它们在哈希表中的相应索引,从而提供对个人信息的直接访问。
下图描述了哈希表及其处理过程,如上一段所述:
随着时间的推移,哈希表在计算场景中变得非常流行。因此,不同的编程语言开始努力在本地或通过内置库提供这种数据结构。
已开发结构的例子有Java中的HashMaps、Python中的dict类(字典)、c++中的map类和Lisp中的list。
哈希表是时间-空间权衡的好例子。如果可用时间是无限的,我们只能将所有键都链接到同一个索引,并执行二进制搜索来恢复特定的数据。
另一方面,如果空间是无限的,我们可以使用完整键作为索引本身,拥有尽可能多的单独内存桶来存储与键对应的数据。
然而,在现实世界中,我们并没有无限的时间或空间。因此,我们将最终处理哈希冲突和索引共享,如下面的小节所述。
3.1 哈希表中的冲突
由于哈希函数将可变长度的键映射到固定长度的索引,因此它们实际上将无限集映射到有限集。以这种方式,碰撞最终会发生。
在哈希表中,冲突意味着哈希函数将多个所需的键映射到相同的索引,从而映射到表的相同内存桶。
因此,提出了许多技术来处理碰撞。我们将简要介绍其中最相关的:
- 单独链表:单独链表技术通过在哈希表的内存桶中支持链表来解决冲突。因此,映射到相同内存桶的数据(键生成相同的索引)被附加到链表中
- 线性探测:也称为开放寻址,这种技术处理冲突,找到确定的具有空闲内存桶以插入数据的索引的第一个索引
- 调整大小和复制:一个简单的技术,调整哈希表的大小,并在碰撞发生时重新分配数据。这个过程的目的是解决一个即时的碰撞问题,并避免在不久的将来发生其他碰撞
4 数据管理的复杂性
哈希表在数据管理方面是一个很好的结构。该数据结构采用的键值方案直观,适合不同场景的多个数据。
此外,在哈希表中搜索、插入和删除数据的平均复杂度是O(1)——一个常数时间。这意味着,无论目标操作是什么,平均而言,单个哈希表查找就足以找到所需的内存桶。
然而,这些操作的最坏情况通常是O(n) -线性时间。当哈希表中的所有数据都具有映射到相同索引的键时,就会出现这种情况。
在这个场景中,哈希表将不断执行一种技术来解决冲突。其中一些技术,如单独链表和线性探测,需要额外的时间来扫描列表或表本身,从而增加了最坏情况下的时间复杂度。
但是,设计良好的哈希表通常很少出现冲突。因此,这种数据结构仍然是保存和提供数据的通用且灵活的选择。
4.1 比较哈希表和其他数据结构
当然,除了哈希表之外,我们还有其他数据结构来管理数据。一个传统的例子是无序链表。在我们的讨论中,让我们考虑一个双链结循环列表的实现。
在链表中,插入和删除一个给定的元素非常简单。最简单的插入操作是追加操作。通过定义的操作次数,我们可以在常量时间(O(1))内向链表添加一个新元素。类似地,给定一个要删除的元素,可以在常数时间内(O(1))执行该操作。
这里需要强调的是,我们认为指向元素的指针已经可用,可以在列表中添加或删除该元素。
然而,关于链表最具挑战性的是搜索特定的元素。在这种情况下,平均复杂度为O(n)。这是因为我们需要检查每个列表元素,直到找到一个特定的元素。
我们可以采用其他技术来降低搜索复杂度,比如二分搜索。但是,在这种情况下,我们需要一个有序的链表,这增加了向列表中插入元素的复杂性。
让我们考虑一个有序的双链循环表。如果我们用插入排序算法保持列表有序,我们的插入复杂度为O(n);删除复杂度仍为O(1);使用二分查找来查找元素,我们的搜索复杂度是O(log n)
下表比较了无序列表、有序列表和哈希表插入、删除和搜索操作的平均时间复杂度:
5 结论
在本教程中,我们学习了哈希表。首先,我们回顾了哈希。因此,我们探索了哈希表的数据结构。在这个上下文中,我们研究了在哈希表中插入、删除和搜索数据的时间复杂度。最后,我们比较了哈希表中这些操作与其他数据管理结构的时间复杂度。
我们可以看到,对于所有考虑的数据管理操作,哈希表具有诱人的平均时间复杂度。特别是,恒定的搜索时间复杂度使得哈希表成为减少算法中循环次数的绝佳资源。
最后,尽管在最坏的情况下具有线性时间复杂度,但平衡良好的哈希函数和维数良好的哈希表自然会避免冲突。所以,最坏情况下的时间复杂度不会发生。