一.什么是哈希表
哈希表(Hash Table)也称为散列表,是一种基于散列函数的从键(key)直接访问对应内存空间的数据结构。由名称中的“表”不难推知,哈希表的一大特点就是加快查找的速度。
了解哈希表,首先要认识散列函数。
散列函数是一种将输入数据映射到固定大小的输出值的函数。它通常用于数据存储和查找中,例如在哈希表中。散列函数的主要目标是将输入数据均匀地分布到输出范围内,以便快速查找和访问数据。
散列函数具有以下属性:
- 决定性:给定相同的输入,散列函数应始终返回相同的输出。
- 均匀性:散列函数应将输入数据均匀地分布到输出范围内,以减少冲突的可能性。
- 高效性:散列函数应具有高效的计算速度,以便在实际应用中能够快速处理大量数据。
- 抗碰撞性:散列函数应尽量避免不同的输入映射到相同的输出,以减少冲突的发生。
由散列函数的定义和性质不难发现,利用上了散列函数高效而精确特性的哈希表,在一般的程序设计中常用来以空间复杂度换取更小时间复杂度,同时不影响原数据结构查找和搜索的准确性。
这样的描述可能非常抽象。用大白话来讲,一个数组就能是一张哈希表,这个数组的索引/下标,就是所谓的键(key),而下标对应位置存入的值(value),就是我们直接访问的“对应内存空间”。
类似于指针,我们生成一张哈希表的时候,就类似于复制了键对应的内存空间里面存着的那个值,并不是真的把那一块内存空间赋给了哈希表里面。而当我们通过哈希表的下标访问这个值的时候,我们得到的值,和我们通过指针去访问对应的那一块内存空间所得到的值,是完全一样的。
例如我们有很多个学生的学号,从1到7,我们把每个学生的名字当成元素存入对应的下标,这个时候下标就和学生的学号对应起来,一张从学号到名字的哈希表就建成了。
上一篇博客中,提到过数组用与模拟的优势,这里就得到了明显的体现。一个数组并没有真正建立从下标到内存空间的映射,它只是在模仿”通过索引访问得到一个值“的这个过程,而c++的STL里的map就是实实在在地做到了这一过程。尽管如此,两者最终得到的结果却是相同的,所以使用起来也不会太过在意其实现这一过程的具体方式。
二.什么时候需要哈希表
一般需要用上哈希表时,多是:
判断一个元素是否在某个集合里出现过
和它的名字一样,哈希表是一张表,如果能打表,应该没有人会不喜欢。。。毕竟时间复杂度只有O(1),除非你的空间真的很紧缺。。。
三.哈希碰撞
一个key有多个value映射过来的时候,我们称这一现象为哈希碰撞。
解决哈希碰撞的方法主要是拉链法和线性探测法。
1.拉链法
拉链法的落脚点在链上,也就是在发生碰撞的索引处建立一个短链表,把多个值链接在一起,查找的时候遍历一遍即可,当然了,链表太长就失去打表的意义了。
2.线性探测法
线性探测法落脚点在线性,也就是在碰撞的索引处开辟额外的连续内存空间。
四.如何实现哈希表
使用数组来模拟生成一张哈希表就不多赘述了。
数组模拟当然容易,但一张散列表的特点就是散,用数组这样的连续线性存储结构是很浪费空间的。为了替代数组,c++的STL中的map和set都提供了便捷可行的模板。
set有三类,分别为set,multiset 和unordered_set,和名字一样,分别是有序不重复集合,有序可重复集合,无序不重复集合。
map同理,也是上述三种:map,multimap和unordered_map。
其中前两种的底层实现都是红黑树(R-B tree),而后一种的实现是哈希表,并且前两种的时间复杂度都是O(logn),而后一种只有O(1),是更优的选择。
五.哈希表的应用
采用数组实现的哈希表:力扣242,383
采用set实现的哈希表:力扣349,202
采用map实现的哈希表:力扣1,15,18,454(分别是二数,三数,四数之和问题)
具体做题的时候,打哈希表也是很有技巧的一件事:
力扣454题,求四个不同数组中四数之和。若是直接采用四个for循环来遍历,肯定是会tle的,而直接一个个遍历打出来哈希表,时间复杂度也高达O(n^3),较好的解决方式是利用分治的思想,把四个遍历分成2+2个遍历,前两个遍历打出A+B的表,后两个遍历进行查找-(C+D)的操作,这样时间复杂度就缩小到了O(n^2),很大但是能过。
但是,并不是所有问题都能用哈希表完美解决的,15和18题就是如此。这两道题多是采用双指针+去重+剪枝来完成,这是解决这类问题的一个通法,理解了原理,实现起来也要注意一些很小的细节。