1. 哈希算法基础与LeetCode实战解析
哈希表作为数据结构中的瑞士军刀,在算法面试中出场率高达70%以上。我在刷完LeetCode全部哈希相关题目后发现,真正需要死记硬背的解法很少,关键是要掌握"空间换时间"的核心思想。下面通过Hot100经典题型,带你拆解哈希的实战应用技巧。
提示:本文代码示例以Java为主,但思路适用于所有语言,文末会提供Python对照版本
1.1 哈希表本质解析
哈希表(Hash Table)本质是键值对存储结构,其高效性来自哈希函数的精心设计。理想状态下:
- 插入/删除时间复杂度:O(1)
- 查找时间复杂度:O(1)
实际工程中需要考虑:
- 哈希冲突处理(开放寻址法/链地址法)
- 负载因子控制(Java HashMap默认0.75)
- 动态扩容机制(容量翻倍+rehash)
java复制// 典型HashMap初始化参数
Map<String, Integer> map = new HashMap<>(16, 0.75f);
2. 两数之和的深度优化
2.1 暴力法到哈希的进化
原始暴力解法O(n²)的缺陷很明显:
python复制for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
哈希解法之所以能降为O(n),核心在于:
- 用Map存储已遍历元素(值→索引)
- 每次检查
target - current是否存在于Map
2.2 边界条件处理实战
java复制class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
map.put(nums[i], i); // 先判断后插入避免自匹配
}
throw new IllegalArgumentException("No solution");
}
}
避坑指南:元素重复时,必须先检查再放入Map。例如target=6,nums=[3,3],如果先put后检查会得到[0,0]的错误结果
3. 字母异位词分组的多解法对比
3.1 排序法(标准解法)
java复制class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String s : strs) {
char[] chars = s.toCharArray();
Arrays.sort(chars);
String key = new String(chars);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(map.values());
}
}
时间复杂度分析:
- 排序:O(klogk)(k为字符串平均长度)
- 遍历:O(n)
- 总体:O(nklogk)
3.2 计数法(优化版本)
python复制def groupAnagrams(strs):
ans = collections.defaultdict(list)
for s in strs:
count = [0] * 26
for c in s:
count[ord(c) - ord('a')] += 1
ans[tuple(count)].append(s)
return list(ans.values())
优势:
- 避免排序操作
- 时间复杂度降为O(nk)
4. 最长连续序列的哈希优化
4.1 排序解法的问题
原始排序解法虽然直观:
java复制Arrays.sort(arr);
但存在两个致命缺陷:
- 时间复杂度O(nlogn)不符合题目要求的O(n)
- 未利用哈希的快速查找特性
4.2 哈希集合法
java复制class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
int longest = 0;
for (int num : numSet) {
if (!numSet.contains(num - 1)) { // 确保从序列起点开始
int currentNum = num;
int currentStreak = 1;
while (numSet.contains(currentNum + 1)) {
currentNum++;
currentStreak++;
}
longest = Math.max(longest, currentStreak);
}
}
return longest;
}
}
关键优化点:
- 使用HashSet去重+快速查找
- 只从序列起点开始统计(即不存在num-1时)
- 时间复杂度严格O(n)(每个元素最多被访问两次)
5. 高频哈希题型扩展
5.1 设计LRU缓存(LeetCode 146)
java复制class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
}
private void addNode(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addNode(node);
}
private DLinkedNode popTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) return -1;
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
DLinkedNode newNode = new DLinkedNode();
newNode.key = key;
newNode.value = value;
cache.put(key, newNode);
addNode(newNode);
++size;
if (size > capacity) {
DLinkedNode tail = popTail();
cache.remove(tail.key);
--size;
}
} else {
node.value = value;
moveToHead(node);
}
}
}
5.2 前缀和+哈希(LeetCode 560)
python复制def subarraySum(nums, k):
count = 0
prefix_sum = 0
sum_map = {0: 1}
for num in nums:
prefix_sum += num
if (prefix_sum - k) in sum_map:
count += sum_map[prefix_sum - k]
sum_map[prefix_sum] = sum_map.get(prefix_sum, 0) + 1
return count
6. 哈希冲突处理实战
当面试官追问"HashMap如何解决冲突"时,应该分层次回答:
-
基础方案:链地址法(Java 8前)
- 数组+链表结构
- 冲突时在链表尾部插入
-
优化方案:红黑树转换(Java 8+)
- 当链表长度>8且桶数量>64时
- 将链表转为红黑树
- 查询时间从O(n)降为O(logn)
-
再哈希法:
- 使用第二个哈希函数计算
- ThreadLocalMap采用此方案
java复制// Java 8 HashMap节点定义
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 链表结构
}
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 红黑树结构
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;
}
7. 性能优化技巧
-
初始容量设置:
java复制// 预估元素数量/0.75 + 1 new HashMap<>(expectedSize * 4 / 3 + 1) -
哈希函数优化:
java复制// String的hashCode实现 public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } -
遍历方式选择:
java复制// 高效遍历方式 for (Map.Entry<K,V> entry : map.entrySet()) { entry.getKey(); entry.getValue(); }
在实际刷题过程中,我发现很多同学容易陷入"知道解法但写不出无bug代码"的困境。建议在IDE中设置以下测试用例:
- 空数组输入
- 重复元素情况
- 超大数量级测试(验证时间复杂度)
- 边界值测试(如Integer.MAX_VALUE)
对于哈希类题目,记住三个核心检查点:
- 是否需要处理重复元素
- 哈希键的选择是否合理
- 是否考虑了遍历顺序的影响