这篇题解是关于前几篇哈希题解的总结篇,题目链接:有效的字母异位词 242、两个数组的交集 349、两数之和 1、三数之和 15、四数之和 18、快乐数 202 和 赎金信 383。主要包含哈希表理论、哈希碰撞、数组,set 和 map 在哈希表中的应用以及N数之和总结。
哈希表理论知识
哈希表是一种根据关键码的值而直接进行访问的数据结构。通俗来讲就是可以通过关键字快速找到数据,就像超市称重台的电子秤,输入一串数字总能显示出对应的商品名,当然超市中的商品值是人为事先编排好的,而哈希表是通过哈希函数实现的。
什么是哈希函数? 如下图所示,将水果通过英文首字母映射到数组中就可以视为简单的哈希映射,而其中这套通过首字母映射的规则就是哈希函数。
一般而言,哈希函数就是通过一些特定的编码方式,将其他数据格式转换为数值的。简单的哈希函数构造法包括直接定址法,除留余数法等等,复杂的哈希函数会有更为严谨的设计和构造过程,例如 Google 开发的 CityHash 就是一种优秀的哈希算法。
哈希碰撞
如下图所示,当列表中增添一个 蓝莓 blueberry 时,会发现映射到数组中也是索引为 1 的元素 b 处,这种情况就称为哈希碰撞,也叫哈希冲突。
实际上,几乎不可能编写出永远能将不同关键字映射到不同位置的哈希函数,因此出现哈希碰撞是很常见的,一般我们通过计算装填因子来判断是否需要对哈希表调整,当装填因子大于0.7时便有必要调整哈希表容量了。装填因子记做 α,计算公式为:
例如下图中哈希表的装填因子就是 :
既然哈希碰撞是很常见的,那么出现哈希碰撞应该怎么解决呢? 常用的两种方法是线性探测法和拉链法,下面是这两种方法的简单操作介绍。
线性探测法
所谓线性探测法就是依靠哈希表中的空位来处理碰撞问题,简单来说就是从碰撞位置不断向下寻找空位,找到第一个空位后便将碰撞值放入,如下图所示:
在这个例子中发生碰撞的位置是 b处 ,向下寻找空位发现的第一个空位是 d处 ,因此便将碰撞值 蓝莓 放到了 d处,这个过程就是简单的线性探测法操作过程。
需要注意的是:
- 哈希表的位置一定大于存放元素的数量,否则将没有空位用来存放碰撞值
- 连续的占用空位形成区块,新数据冲突时更容易落到这块区域,冲突扎堆、越长越大
- 不能直接删除某个元素,应该设置删除标记,否则整个线性查询将中断
拉链法
拉链法是指直接在发生碰撞位置设置链表,依次存储碰撞值的方法,如下图所示:
也要注意:
- 如果大量元素集中在同一个链表,链表过长,查询效率退化至
- 需要额外开销存储链表指针,存在少量内存开销
数组在哈希表中的应用
哈希数组的应用主要体现在两道题目中,分别是 有效的字母异位词 242 和 赎金信 383 。
实际上,这两道题目的大体思路很相似:
- 定义长度为 26 的
record数组用于记录各字母出现次数 - 先对第一个数据进行遍历,各个字母每出现一次,对应索引处元素便
+1 - 再对第二个数据遍历,各字母每出现一次,
record中对应元素便-1 - 最后通过判断
record数组中各元素是否为零
在哈希表中要使用数组需要满足的条件是元素有限、范围连续且数量较少,例如上述两题元素均为小写字母。这是因为数组的操作原理是先划分空间再进行存储,因此要求有限个,而连续且少量是因为数据范围过广,量过大时再使用数组会占用大量内存。
哈希数组的本质是直接寻址哈希表,不存在哈希碰撞,效率高于其他容器的哈希表,但缺点是需要提前开辟整块空间,键范围跨度很大时不能使用,这时就需要选择 map。
set容器在哈希表中的应用
两个数组的交集 349 是使用 set容器 解决哈希问题的典型例题,在题解中对比了四类常见容器的特点并得出由于题给条件是 单值无序 因此选用 unordered_set 是最优解。当然在力扣后期修改题目后给定了数据范围,这道题也就可以使用哈希数组来实现。其简单思路如下图:
在 C++ 中,提供了三种 set 容器,分别是 set、multiset 和 unordered_set 。其中set和multiset的底层结构是红黑树,而unordered_set的底层结构是哈希表。
在 快乐数 202 中也使用了 unordered_set 哈希结构,用于判断 sum 是否重复出现。同时这道题目还可以使用 快慢指针法 解决,通过成环后两指针相遇位置对应的数值来判断是否为快乐数,具体原理详见本篇题目题解以及 环形链表Ⅱ 142 。
map容器在哈希表中的应用
map 在哈希表中的应用主要体现在 两数之和 1 ,相较于数组和 set ,map 的优点在于 map 是一种<key, value>的结构,在本题中可以用 key 保存数值,用 value 保存数值所在的下标。
与 set 容器类似,C++也提供了三种 map 容器:map、multimap 和 unordered_map,其中map和multimap的底层结构是红黑树,unordered_map的底层结构是哈希表。
与 set 容器同理,map 和 multimap 的 key 也是有序的,而本题中并不需要 key 有序,因此选择 unordered_map 效率更高。
N数之和
N数之和是一类典型算法题,主要包含 两数之和 1、三数之和 15 和 四数之和 18 。其中两数之和使用 map 哈希容器容易解题,而三数之和与四数之和则更适合使用双指针解题,根本原因是双指针可以有效去重,而哈希处理三数、四数去重极其麻烦。
三数之和与四数之和的算法逻辑大体相似:
- 排序数组(双指针前提,同时方便去重)
- 递归 / 循环固定前
k-2个数 - 最后剩下两个数,使用双指针查找
- 每一层循环都进行去重,避免重复组合
两数之和不适合使用双指针,核心原因是双指针需要排序,排序后原始下标会丢失,无法返回索引,而 map 正好有 key 和 value 两个值,可以同时存储,因此两数之和更适合用哈希。
总结
哈希刷题快速选型:
- 字符 / 数字范围连续且很小 → 数组哈希,效率最高
- 仅需判断元素是否存在、无需存储附加信息 → unordered_set
- 需要键值配对存储(下标、计数)→ unordered_map
- 需要自动有序结果 → set/map
- 两数之和要下标用 unordered_map
- N数之和(除两数之和)优先排序 + 双指针。