哈希表(Hash Table)作为数据结构中的瑞士军刀,在算法面试和实际工程中都有着举足轻重的地位。今天我将通过LeetCode Hot 100中的四道经典题目,带大家深入掌握哈希技术的核心思想和实战技巧。这些题目覆盖了哈希表最典型的三种实现形式:数组哈希、集合(Set)和映射(Map),每种形式都有其独特的适用场景和优化逻辑。
提示:在实际面试中,面试官往往会通过这四类问题考察候选人对哈希本质的理解程度。理解为什么选择特定实现方式比记住解法更重要。
哈希表的魔力在于它平均O(1)的时间复杂度,这源于哈希函数将键(key)直接映射到存储位置的特性。想象一个超大型图书馆,普通查找需要遍历书架(O(n)),而哈希就像给每本书配备了精确的GPS坐标,可以直接定位。
三种典型实现方式:
242题要求判断两个字符串是否为字母异位词(anagram),即字母组成相同但顺序不同。最直观的解法是对字符串排序后比较,但时间复杂度为O(nlogn),不够优雅。
java复制// 暴力排序解法
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
char[] sArr = s.toCharArray();
char[] tArr = t.toCharArray();
Arrays.sort(sArr);
Arrays.sort(tArr);
return Arrays.equals(sArr, tArr);
}
观察到字母数量有限(英文小写字母26个),我们可以使用int[26]作为哈希表:
java复制public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
int[] hash = new int[26];
// 计数阶段
for(char c: s.toCharArray()) hash[c - 'a']++;
// 验证阶段
for(char c: t.toCharArray()) hash[c - 'a']--;
// 检查阶段
for(int count : hash)
if(count != 0) return false;
return true;
}
c - 'a'将字母映射到0-25的索引注意:当处理Unicode字符时,数组大小会爆炸,此时应该改用HashMap。这也是工程中更通用的做法。
349题要求找出两个数组的交集,且结果唯一。这里有几个关键约束:
当数值范围较小时(如题目提示的<=1000),数组哈希是最佳选择:
java复制public int[] intersection(int[] nums1, int[] nums2) {
int[] hash = new int[1001]; // 假设数值范围0-1000
for(int num : nums1)
if(hash[num] == 0) hash[num] = 1;
int[] tmp = new int[1001];
int cnt = 0;
for(int num : nums2) {
if(hash[num] == 1) {
tmp[cnt++] = num;
hash[num] = 2; // 标记已处理
}
}
int[] res = new int[cnt];
System.arraycopy(tmp, 0, res, 0, cnt);
return res;
}
当数值范围很大或未知时,HashSet是更安全的选择:
java复制public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set1 = new HashSet<>();
Set<Integer> resultSet = new HashSet<>();
for(int num : nums1) set1.add(num);
for(int num : nums2) {
if(set1.contains(num))
resultSet.add(num);
}
return resultSet.stream().mapToInt(i->i).toArray();
}
| 实现方式 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 数组哈希 | O(n+m) | O(1) | 数值范围小且已知 |
| HashSet | O(n+m) | O(n) | 通用场景,数值范围大 |
202题"快乐数"的定义看似简单,但隐藏着重要的算法思想——循环检测。根据数学证明:
使用HashSet记录出现过的数字,一旦重复立即返回false:
java复制public boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while(n != 1 && !seen.contains(n)) {
seen.add(n);
n = getNext(n);
}
return n == 1;
}
private int getNext(int n) {
int sum = 0;
while(n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
可以不使用额外空间,用双指针检测循环:
java复制public boolean isHappy(int n) {
int slow = n;
int fast = getNext(n);
while(fast != 1 && slow != fast) {
slow = getNext(slow);
fast = getNext(getNext(fast));
}
return fast == 1;
}
这种方法将空间复杂度从O(logn)降到了O(1),但理解难度稍高。
1题"两数之和"是哈希表最经典的案例,要求在数组中找到和为target的两个数:
java复制public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> numToIndex = new HashMap<>();
for(int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if(numToIndex.containsKey(complement)) {
return new int[]{numToIndex.get(complement), i};
}
numToIndex.put(nums[i], i);
}
return null;
}
虽然排序后可以用双指针找到数值解,但会破坏原始索引:
java复制public int[] twoSum(int[] nums, int target) {
int[][] numsWithIndex = new int[nums.length][2];
for(int i = 0; i < nums.length; i++) {
numsWithIndex[i][0] = nums[i];
numsWithIndex[i][1] = i;
}
Arrays.sort(numsWithIndex, (a, b) -> a[0] - b[0]);
int left = 0, right = nums.length - 1;
while(left < right) {
int sum = numsWithIndex[left][0] + numsWithIndex[right][0];
if(sum == target) {
return new int[]{numsWithIndex[left][1], numsWithIndex[right][1]};
} else if(sum < target) {
left++;
} else {
right--;
}
}
return null;
}
| 方法 | 时间复杂度 | 空间复杂度 | 保持顺序 | 适用场景 |
|---|---|---|---|---|
| 哈希表 | O(n) | O(n) | 是 | 通用场景 |
| 排序+双指针 | O(nlogn) | O(n) | 否 | 需要节省空间时 |
Java的HashMap默认负载因子是0.75,当元素数量达到容量75%时自动扩容。理解这点对性能敏感场景很重要:
java复制// 预估元素数量时指定初始容量
Map<Integer, Integer> map = new HashMap<>(expectedSize * 4 / 3 + 1);
开放寻址法:发生冲突时寻找下一个空槽
链地址法:Java HashMap采用的方式
重写equals()必须同时重写hashCode():
java复制class Person {
String name;
int age;
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object o) {
if(this == o) return true;
if(!(o instanceof Person)) return false;
Person p = (Person)o;
return age == p.age && name.equals(p.name);
}
}
java复制// 错误示范:频繁扩容
Set<Integer> set = new HashSet<>();
// 正确做法
Set<Integer> set = new HashSet<>(nums.length);
java复制// 原始类型优先
Map<Integer, Integer> map = new HashMap<>();
// 更优方案(Android等环境)
SparseIntArray sparseArray = new SparseIntArray();
java复制// 糟糕的hashCode实现
@Override
public int hashCode() {
return 42; // 所有对象相同哈希值
}
哈希统计模板:
存在性检查模板:
在实际刷题过程中,我建议先从数组哈希开始理解基本原理,再逐步过渡到更复杂的HashSet和HashMap应用。记住,哈希技术的核心思想是"用空间换时间",但如何平衡空间和时间才是算法设计的精髓所在。