哈希表(Hash Table)是计算机科学中最重要的数据结构之一,也是算法面试中的高频考点。它通过键值对(key-value)的形式存储数据,能够在平均O(1)时间复杂度内完成数据的插入、删除和查找操作。
哈希表的核心在于哈希函数的设计。当我们插入一个键值对时:
以Java中的HashMap为例,当我们执行map.put("apple", 1)时:
注意:好的哈希函数应该尽可能减少冲突(不同key产生相同hash值的情况),同时计算速度要快。
当不同key产生相同hash值时,就需要处理冲突。常见方法有:
链地址法(Separate Chaining):
开放寻址法(Open Addressing):
再哈希法:
哈希表最直接的应用就是快速查找。比如在LeetCode第一题"两数之和"中,我们可以使用哈希表将查找时间从O(n²)优化到O(n):
java复制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 two sum solution");
}
哈希表非常适合用于统计元素出现频率。例如统计字符串中每个字符出现的次数:
python复制from collections import defaultdict
def count_chars(s):
freq = defaultdict(int)
for char in s:
freq[char] += 1
return freq
像Redis这样的内存数据库,其核心就是基于哈希表实现的键值存储。浏览器缓存、DNS缓存等也都依赖哈希表的高效查找特性。
负载因子(Load Factor)是哈希表中已存储元素数量与总容量的比值。当负载因子超过阈值(通常为0.75)时,哈希表会自动扩容以避免性能下降。
以Java HashMap为例:
提示:如果能预估元素数量,最好在创建哈希表时指定初始容量,避免频繁扩容带来的性能损耗。
一个好的哈希函数应该满足:
Java中Object类的hashCode()方法就是一个基础的哈希函数实现。对于自定义类,重写hashCode()时通常需要考虑所有参与equals比较的字段。
当恶意攻击者故意构造大量hash冲突的key时,会导致哈希表退化为链表,性能急剧下降。解决方案包括:
大多数哈希表实现(如HashMap)不是线程安全的。在多线程环境下,应该使用:
哈希表虽然查询快,但内存占用较高。在内存敏感的场景下,可以考虑:
在处理数组/链表问题时,双指针和哈希表经常配合使用。例如"三数之和"问题:
python复制def threeSum(nums):
nums.sort()
res = []
for i in range(len(nums)-2):
if i > 0 and nums[i] == nums[i-1]:
continue
l, r = i+1, len(nums)-1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s < 0:
l +=1
elif s > 0:
r -= 1
else:
res.append([nums[i], nums[l], nums[r]])
while l < r and nums[l] == nums[l+1]:
l += 1
while l < r and nums[r] == nums[r-1]:
r -= 1
l += 1; r -= 1
return res
处理子数组求和问题时,前缀和+哈希表是经典解法。例如"和为K的子数组":
java复制public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
int sum = 0, count = 0;
for (int num : nums) {
sum += num;
if (map.containsKey(sum - k)) {
count += map.get(sum - k);
}
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
return count;
}
在处理子串问题时,滑动窗口配合哈希表可以高效解决。例如"最小覆盖子串":
python复制from collections import defaultdict
def minWindow(s: str, t: str) -> str:
need = defaultdict(int)
for c in t:
need[c] += 1
needCnt = len(t)
i = 0
res = (0, float('inf'))
for j, c in enumerate(s):
if need[c] > 0:
needCnt -= 1
need[c] -= 1
if needCnt == 0:
while True:
c = s[i]
if need[c] == 0:
break
need[c] += 1
i += 1
if j - i < res[1] - res[0]:
res = (i, j)
need[s[i]] += 1
needCnt += 1
i += 1
return '' if res[1] > len(s) else s[res[0]:res[1]+1]
Java的HashMap有几个重要特性:
Python的字典实现特点:
C++的unordered_map:
根据预估元素数量设置初始容量,避免扩容开销。例如在Java中:
java复制// 预计存储1000个元素
Map<String, Integer> map = new HashMap<>(1333); // 1000/0.75
对于特定场景,可以自定义更高效的哈希函数。例如处理字符串时:
java复制public int hashString(String s) {
int hash = 0;
for (char c : s.toCharArray()) {
hash = 31 * hash + c;
}
return hash;
}
对于原始类型,使用专门的数据结构可以减少性能开销。例如在Java中:
java复制// 使用原始类型专有的Map
Int2ObjectOpenHashMap<String> map = new Int2ObjectOpenHashMap<>();
给定一个字符串数组,将字母异位词组合在一起:
python复制def groupAnagrams(strs):
from collections import defaultdict
ans = 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())
给定未排序的整数数组,找出最长连续序列的长度:
java复制public int longestConsecutive(int[] nums) {
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
int longestStreak = 0;
for (int num : numSet) {
if (!numSet.contains(num-1)) {
int currentNum = num;
int currentStreak = 1;
while (numSet.contains(currentNum+1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak;
}
深度拷贝一个带有随机指针的链表:
python复制def copyRandomList(head):
if not head:
return None
mapping = {}
# 第一遍遍历创建所有节点
curr = head
while curr:
mapping[curr] = Node(curr.val)
curr = curr.next
# 第二遍遍历设置next和random
curr = head
while curr:
if curr.next:
mapping[curr].next = mapping[curr.next]
if curr.random:
mapping[curr].random = mapping[curr.random]
curr = curr.next
return mapping[head]
数据库中的哈希索引就是基于哈希表实现的,适合等值查询但不适合范围查询。MySQL的Memory引擎就支持哈希索引。
Web应用中的会话(Session)通常使用哈希表来存储,键是session ID,值是会话数据。
像Memcached这样的分布式缓存系统,其核心就是基于一致性哈希算法实现的键值存储。
初级阶段:
中级阶段:
高级阶段:
实战阶段:
哈希表作为基础数据结构,其重要性怎么强调都不为过。在实际编程中,我经常发现很多性能问题都可以通过合理使用哈希表来解决。特别是在处理数据关联和快速查找场景时,哈希表往往能带来数量级的性能提升。