1、为什么要设计哈希表?
2、哈希表的工作原理是什么?
3、如何解决哈希冲突?
4、哈希表有什么优缺点?
5、哈希表如何扩容?
一、为什么要设计哈希表
哈希表的出现是为了解决数组和链表在查找和插入操作上的局限性
-
数组的问题:数组在插入和删除元素时需要移动大量的元素来调整顺序,导致操作效率较低。此外,数组的大小固定,无法动态调整。
-
链表的问题:链表在插入和删除元素时只需要调整相邻节点的指针,操作效率较高。但是,链表的查找操作效率较低,需要从头节点开始遍历整个链表直到找到目标元素。
散列表的目的是通过哈希函数将数据映射到一个固定的位置(索引)上,从而使插入和查找操作的效率更高。
二、哈希表的工作原理
- 首先,使用哈希函数将键(key)转换成一个索引值,该索引值对应散列表中的一个位置。
- 将值(value)存储在对应的索引位置上,或者使用额外的数据结构(如链表)来解决散列冲突问题。
- 当需要查找或删除一个元素时,再次使用哈希函数将键(key)转换成索引值,然后在对应的位置上查找或删除值。
散列表的优势在于具有常数级O(1)别的插入和查找时间复杂度(平均情况下),而不受数据集大小的限制。它可以在大量数据中快速准确地查找目标元素,比起数组和链表更为高效。
三、如何解决哈希冲突
哈希冲突是指两个或多个不同的键通过哈希函数计算得到相同的索引位置。解决哈希冲突的常见方法有两种:开放地址法和链地址法。
- 开放地址法(Open Addressing):
- 线性探测(Linear Probing):当发生冲突时,顺序地查找下一个可用的空槽,直到找到一个空槽来存储冲突的元素。
- 二次探测(Quadratic Probing):当发生冲突时,通过一定的增量(如平方函数)逐渐查找下一个可用的空槽。
- 双重散列(Double Hashing):当发生冲突时,利用第二个哈希函数计算一个增量值,并基于增量值查找下一个可用的空槽。
- 链地址法(Chaining):
- 每个哈希桶都是一个链表,当发生冲突时,冲突的元素被插入到对应桶的链表中。这样,多个具有相同哈希值的元素可以共享同一个桶,并且不影响其他元素。
- 当需要查找或删除一个元素时,通过哈希函数定位到对应的桶,然后在该桶的链表中进行操作。
开放地址法和链地址法都可以有效地解决哈希冲突问题。选择哪种方法取决于实际应用场景和数据特征。在开放地址法中,随着装载因子的增加,线性探测和二次探测可能会导致聚集效应,影响性能。而链地址法需要额外的内存来存储链表结构,但可以灵活地处理冲突。
在实现散列表时,通常会根据具体情况选择合适的解决冲突的方法,并可以通过调整参数和策略来优化散列表的性能。
疑问?为什么线性探测和二次探测可能会导致聚集效应为啥会影响性能?
答:线性探测和二次探测都是哈希表中解决冲突的方法之一。这两种探测方法使用同一张哈希表,当发生哈希冲突时,它们会采用不同的方式来解决。
线性探测法:当插入值时,如果哈希桶中的位置被占用,则依次向后探测下一个位置,直到找到一个空位置或者抵达哈希表的末尾。这种方法的缺点是容易导致聚集效应。当冲突增多时,发生聚集效应的概率也会增加,因为需要探测的位置都被占用了,导致效率下降。
二次探测法:与线性探测相似,只是它是给定一个初始的偏移量,而不是线性地向后移动。如果初始位置被占用,则将偏移量的平方加到初始位置,以便继续在哈希表中搜索下一个节点,而不是直接移动到相邻的位置。这种方法同样容易导致聚集效应。如果哈希表中使用的是二次探测法,那么当探测的位置被占用时,偏移量的增加可能会使聚集效应更加明显。
聚集效应会导致哈希表的性能变得更差,因为它会增加哈希表查找和插入的时间,可能会降低哈希表的效率和吞吐量。避免聚集效应的方法包括缩小哈希表中的链的长度,减小负载因子,以及使用更好的哈希函数。
四、哈希表的优缺点
优点:
- 快速访问:哈希表通过哈希函数计算得到键值对应的索引,访问数据的速度非常快,时间复杂度为O(1)。
- 空间利用:哈希表的空间利用效率比较高,因为元素不需要按照顺序排列在连续的内存空间中,元素的存储位置是根据哈希函数计算得到的索引分散在内存中。
- 高效的插入和删除:哈希表的插入和删除操作只需要对元素进行一次哈希计算就可以完成,因此,相对于其他数据结构,插入和删除的时间复杂度较低。
缺点:
- 哈希冲突:由于哈希函数在将不同的键值映射到同一个位置时会产生哈希冲突,这会导致查找、插入、删除等操作变得复杂,需要额外的操作来处理哈希冲突。
- 哈希函数的设计:哈希函数的设计非常重要,一个好的哈希函数可以使哈希表的性能非常优秀,而不利于数据结构的性能。因此,设计一个好的哈希函数需要考虑多个因素,如键值的分布等。
- 内存消耗:哈希表需要消耗较多的内存空间,因为为了保证元素的快速查找,哈希表需要比实际元素数量多分配一些数组空间。
- 无序性:哈希表是无序的数据结构,无法保证元素在哈希表中的顺序和插入的顺序一致。如果需要按特定顺序访问元素,获得有序的结果需要进行额外的操作。
五、哈希表的扩容
哈希表的内部是通过数组实现的,既然是数组就会有大小,当元素过多时就会导致数组大小不够承载这么多元素,就需要进行扩容,此外哈希表在存储大量元素时,可能会面临哈希冲突等问题,造成查找和插入操作的效率下降。针对这个问题,扩容是一种常见而有效的优化方案。
哈希表的扩容过程通常包括以下几个步骤:
- 创建一个新的哈希表,并将容量扩大为原来的两倍或其他倍数,在新表中分配新的数组空间。
- 遍历旧哈希表中的所有元素,并重新计算它们在新数组中的地址。
- 将元素插入到新表中,可以选择新的哈希函数,或者保留原有的哈希函数不变。如果使用相同的哈希函数,可能需要在旧表和新表之间移动元素。
- 释放旧哈希表的内存空间,将新哈希表赋值给旧哈希表,完成扩容操作。
需要注意以下几点:
- 扩容操作通常要尽可能避免对用户造成数据丢失或数据不可用的影响,因此需要考虑并发操作和线程安全性。
- 在实际应用中,为了尽可能少地进行扩容操作,通常会在哈希表使用超过一定比例时(通常为数组大小的0.7倍),开始启动扩容操作。否则如果每插入一个元素就要扩容,就会造成不必要的性能损失。
- 如果哈希表中的元素过于稠密,扩容操作可以增加利用率,提高哈希表的性能。但是,如果哈希表中有大量的空桶,扩容操作的效果就不是很好,并且增加了内存开销。
总之,哈希表的扩容操作是一种优化哈希表性能的有效方法,需要综合考虑多方面因素,使用合适的算法和数据结构,并确保在线性时间内进行。