LeetCode热题100的哈希表解法,我用这5道题给你讲透了(两数之和/字母异位词分组/最长连续序列)
LeetCode热题100的哈希表解法:5道经典题目深度解析
哈希表(Hash Table)作为算法面试中的常客,几乎在每场技术面试中都会出现。它之所以备受青睐,是因为其平均时间复杂度为O(1)的查找效率,能够将许多看似复杂的问题简化为线性时间解决。本文将深入剖析LeetCode热题100中最具代表性的5道哈希表题目,从键值设计到底层实现原理,带你彻底掌握这一数据结构的精髓。
1. 两数之和:哈希表的入门经典
作为LeetCode的第一题,"两数之和"完美展示了哈希表的核心价值。题目要求在一个整数数组中找到两个数,使它们的和等于目标值,并返回这两个数的索引。
暴力解法的O(n²)时间复杂度显然不够高效。而哈希表的引入可以将时间复杂度降至O(n):
关键点解析:
- 哈希表存储的是
值->索引的映射 - 在遍历时先检查
target - current_num是否存在于哈希表中 - 如果存在则立即返回,否则将当前数字存入哈希表
注意:这里必须先检查再插入,否则当target是当前数字的两倍时会出现错误结果
时间复杂度分析:
- 单次哈希表查找和插入的平均时间复杂度都是O(1)
- 遍历整个数组一次,因此总时间复杂度为O(n)
空间复杂度为O(n),因为最坏情况下需要存储所有元素。
2. 字母异位词分组:巧妙的键设计
字母异位词分组问题要求将一组字符串按照字母异位词(由相同字母重新排列组成的单词)进行分组。例如["eat","tea","tan","ate","nat","bat"]应该分组为[["eat","tea","ate"],["tan","nat"],["bat"]]。
哈希表解法的关键在于如何设计哈希表的键。常见的有三种方法:
- 排序字符串作为键:
- 字符计数作为键:
- 质数乘积作为键(避免排序开销):
性能对比:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 排序键 | O(n*klogk) | O(nk) | 通用,实现简单 |
| 计数键 | O(n*k) | O(nk) | 字符范围有限时更优 |
| 质数键 | O(n*k) | O(nk) | 需要极致性能时 |
提示:面试中通常采用排序键的方法即可,但能讨论其他方法会加分
3. 最长连续序列:哈希集合的妙用
最长连续序列问题要求在未排序的整数数组中找到最长连续元素序列的长度。例如,给定[100,4,200,1,3,2],最长连续序列是[1,2,3,4],因此返回4。
暴力解法需要对每个数字检查其+1是否存在,时间复杂度为O(n³)。而使用哈希集合可以将时间复杂度降至O(n):
算法解析:
- 首先将所有数字存入哈希集合,实现O(1)时间的存在性检查
- 对于每个数字,只有当它是序列的起点(即num-1不存在)时才进行处理
- 向后检查连续的数字,更新最长序列长度
为什么时间复杂度是O(n)?
- 虽然看起来有嵌套循环,但每个数字最多被访问两次(一次在外部循环,一次在内部循环)
- 因此总体时间复杂度仍然是O(n)
4. 存在重复元素II:滑动窗口与哈希表结合
这道题要求判断数组中是否存在两个不同的索引i和j,使得nums[i] == nums[j]并且i和j的绝对差不超过k。
暴力解法需要双重循环,时间复杂度为O(n²)。使用哈希表可以优化到O(n):
优化思路:
- 维护一个哈希表记录数字到最近出现位置的映射
- 遍历时检查当前数字是否在哈希表中且位置差≤k
- 无论是否满足条件,都更新哈希表中该数字的位置为当前位置
变种问题:如果k很大,可以考虑使用滑动窗口+哈希集合:
这种方法的空间复杂度是O(k),当k远小于n时更节省空间。
5. 四数相加II:哈希表处理组合问题
给定四个整数数组A、B、C、D,计算有多少个元组(i,j,k,l)使得A[i]+B[j]+C[k]+D[l]=0。
暴力解法需要四重循环,时间复杂度O(n⁴)。使用哈希表可以优化到O(n²):
算法步骤:
- 首先计算A和B中所有元素两两之和的频率,存入哈希表
- 然后计算C和D中所有元素两两之和的相反数
- 在哈希表中查找这些相反数,累加对应的频率
性能分析:
- 时间复杂度:O(n²)(两个O(n²)的循环)
- 空间复杂度:O(n²)(最坏情况下A+B的所有组合都不同)
提示:这类"分组+哈希表"的思路也适用于三数之和等问题,是处理组合问题的有效技巧
哈希表的高级应用与底层原理
理解了这些经典问题的解法后,我们还需要深入哈希表的底层实现,才能在面试中游刃有余。
哈希冲突��理
C++中的unordered_map和Python中的dict都使用开放地址法处理冲突。当发生冲突时,常见的解决方法有:
- 链地址法:每个桶存储一个链表
- 开放地址法:按照某种探测序列寻找下一个空桶
- 线性探测:h(k,i) = (h'(k)+i) mod m
- 平方探测:h(k,i) = (h'(k)+c₁i+c₂i²) mod m
- 双重哈希:h(k,i) = (h₁(k)+i*h₂(k)) mod m
负载因子与扩容
哈希表的性能与负载因子(元素数量/桶数量)密切相关。当负载因子超过阈值(通常为0.75)时,哈希表会进行扩容:
- 分配一个新的更大的桶数组
- 重新计算所有元素的哈希值并插入新数组
- 释放旧数组
扩容操作的时间复杂度:
- 均摊分析下仍然是O(1),但单次插入的最坏情况是O(n)
语言实现差异
| 语言 | 实现类 | 冲突解决 | 是否有序 | 线程安全 |
|---|---|---|---|---|
| C++ | unordered_map | 开放地址法 | 无序 | 不安全 |
| Java | HashMap | 链地址法 | 无序 | 不安全 |
| Python | dict | 开放地址法 | 3.7+有序 | 不安全 |
在实际编码中,理解这些差异有助于选择最适合的数据结构。例如,如果需要有序性,C++中应该使用map而非unordered_map。