哈希表(Hash Table)是一种基于键值对存储的数据结构,它通过哈希函数将键映射到表中特定位置来实现快速数据访问。这种设计使得哈希表在理想情况下能够实现O(1)时间复杂度的查找操作,相比线性查找的O(n)有显著性能提升。
哈希表的核心在于哈希函数的设计,它需要满足两个关键特性:
在实际应用中,我们还需要处理哈希冲突(不同键映射到同一位置的情况)。常见的冲突解决方法包括:
提示:好的哈希函数应该尽量减少冲突概率,同时计算不能过于复杂。Java中Object类的hashCode()方法提供了基础实现,但自定义类通常需要重写此方法。
HashMap是Java中最常用的哈希表实现,它使用数组+链表(JDK8后加入红黑树)的结构:
java复制// 初始化一个HashMap
Map<Integer, String> map = new HashMap<>();
// 常用操作示例
map.put(1, "Apple"); // 插入键值对
String value = map.get(1); // 获取值
boolean exists = map.containsKey(1); // 检查键是否存在
map.remove(1); // 删除键值对
HashMap的几个关键特性:
HashSet实际上是基于HashMap实现的,它只使用键而将值设为固定对象:
java复制// HashSet内部实现简化示意
public class HashSet<E> {
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
}
使用示例:
java复制Set<Integer> set = new HashSet<>();
set.add(1); // 添加元素
boolean contains = set.contains(1); // 检查存在性
set.remove(1); // 移除元素
注意:HashSet的迭代顺序是不确定的,如果需要有序集合,可以考虑LinkedHashSet或TreeSet。
字母异位词是指由相同字母重新排列形成的不同单词。判断两个字符串是否为字母异位词,本质上是要确认:
最直观的解法是使用长度为26的数组统计每个字母出现次数:
java复制public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
int[] counts = new int[26];
for (char c : s.toCharArray()) {
counts[c - 'a']++;
}
for (char c : t.toCharArray()) {
counts[c - 'a']--;
if (counts[c - 'a'] < 0) return false;
}
return true;
}
时间复杂度:O(n),空间复杂度:O(1)(固定大小的数组)
另一种思路是对字符串排序后直接比较:
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);
}
时间复杂度:O(nlogn),空间复杂度:O(n)(字符数组)
如果考虑Unicode字符,数组大小需要扩展。可以使用HashMap来通用化解决方案:
java复制public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
Map<Character, Integer> map = new HashMap<>();
for (char c : s.toCharArray()) {
map.put(c, map.getOrDefault(c, 0) + 1);
}
for (char c : t.toCharArray()) {
int count = map.getOrDefault(c, 0);
if (count == 0) return false;
map.put(c, count - 1);
}
return true;
}
实操技巧:在处理字符串问题时,明确字符集范围很重要。如果是纯小写字母,数组方案更高效;如果是Unicode,HashMap更合适。
求两个数组的交集,核心是找出同时存在于两个数组中的元素。HashSet因其O(1)的查找特性非常适合这类问题。
java复制public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set1 = new HashSet<>();
for (int num : nums1) set1.add(num);
Set<Integer> result = new HashSet<>();
for (int num : nums2) {
if (set1.contains(num)) result.add(num);
}
return result.stream().mapToInt(i->i).toArray();
}
当元素范围有限时(如题目提示0-1000),可以用数组替代HashSet:
java复制public int[] intersection(int[] nums1, int[] nums2) {
boolean[] mark = new boolean[1001];
for (int num : nums1) mark[num] = true;
List<Integer> res = new ArrayList<>();
for (int num : nums2) {
if (mark[num]) {
res.add(num);
mark[num] = false; // 避免重复添加
}
}
return res.stream().mapToInt(i->i).toArray();
}
如果数组已排序,可以使用双指针法进一步优化空间:
java复制public int[] intersection(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
int i = 0, j = 0;
Set<Integer> set = new HashSet<>();
while (i < nums1.length && j < nums2.length) {
if (nums1[i] == nums2[j]) {
set.add(nums1[i]);
i++;
j++;
} else if (nums1[i] < nums2[j]) {
i++;
} else {
j++;
}
}
return set.stream().mapToInt(k->k).toArray();
}
性能对比:HashSet解法平均O(m+n)时间,数组法在有限范围内更优,双指针法在已排序情况下最优(O(max(mlogm, nlogn)))。
快乐数的判定关键在于识别循环。根据数学理论,任何数的数字平方和计算过程要么收敛到1,要么进入4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4的循环。
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 d = n % 10;
sum += d * d;
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;
}
利用数学知识,可以直接硬编码循环检测:
java复制public boolean isHappy(int n) {
while (n != 1 && n != 4) {
n = getNext(n);
}
return n == 1;
}
深度理解:数字平方和的计算过程实际上定义了一个有向图,快乐数问题等价于判断这个图是否最终到达1这个固定点。
这是哈希表应用的经典案例,通过"空间换时间"将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 solution");
}
需要注意的特殊情况包括:
类似的问题变种包括:
java复制// 有序数组的两数之和解法
public int[] twoSumSorted(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
return new int[]{left, right};
} else if (sum < target) {
left++;
} else {
right--;
}
}
throw new IllegalArgumentException("No solution");
}
性能提示:在Java中,HashMap的自动装箱(int→Integer)会有额外开销。对于性能敏感场景,可以考虑使用Trove或Eclipse Collections等第三方库的原始类型Map实现。
java复制// 预估有1000个元素,负载因子0.75
new HashMap<>(1333); // 1000/0.75 ≈ 1333
java复制// 简单的LRU缓存实现示例
class LRUCache {
private LinkedHashMap<Integer, Integer> map;
private final int CAPACITY;
public LRUCache(int capacity) {
CAPACITY = capacity;
map = new LinkedHashMap<>(capacity, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > CAPACITY;
}
};
}
public int get(int key) {
return map.getOrDefault(key, -1);
}
public void put(int key, int value) {
map.put(key, value);
}
}
哈希表作为基础数据结构,其高效性和灵活性使其成为算法设计和系统开发中的核心工具。理解其内部机制和适用场景,能够帮助开发者写出更高效、更健壮的代码。在实际工程中,除了考虑时间复杂度,还需要关注内存使用、并发安全和实际运行效率等因素。