算法应用场景
在算法面试中,哈希表(Hash Table) 是优化时间复杂度的最高效神器。它的核心优势在于:能将数据的存储和查找操作降低至 $O(1)$ 的惊人复杂度。
通常,当你发现代码中出现了两层 for 循环嵌套,或者需要频繁查找某个元素是否存在时,第一时间就应该想一想:能不能用哈希表把这一层循环给“吃”掉?
通过拆解 LeetCode Hot 100 中的哈希表核心题目,本质上只有两种核心套路:「分类与归纳模板」和「逆向查找模板」。掌握了这两个基础模板,你就能秒杀绝大多数哈希题型。
🚀 模板一:分类与特征归纳模板
1. 模板抽象与核心思想
这类题目的本质是:把一堆乱七八糟的数据,按照某种“共同特征”分门别类地放进不同的格子里。 我们使用最传统的 if-else 来判断格子是否存在,如果不存在就新建一个格子。
// 通用伪代码模板
Map<String, List<数据类型>> map = new HashMap<>();
for (数据类型 item : inputs) {
// 1. 算出当前数据的“特征 key”
String key = 算出特征(item);
// 2. 如果这个特征在 Map 里还没有建格子,就先建一个空格子
if (!map.containsKey(key)) {
map.put(key, new ArrayList<>());
}
// 3. 把当前数据放进对应的格子里
map.get(key).add(item);
}
2. Hot 100 实战演练:LeetCode 49. 字母异位词分组
题目大意:给你一个字符串数组 strs,请你将字母异位词组合在一起。字母异位词是由重新排列源单词的字母得到的新单词(比如 "eat" 和 "tea" 乱序后字符都一样,它们属于同一组)。
🛠️ 纯基础款代码实现
按照模板,这道题的“特征 key”就是:把字符串拆开排序,排完序后 "eat" 和 "tea" 都会变成 "aet"。这个 "aet" 就是它们的公共格子。
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 边界情况处理
if (strs == null || strs.length == 0) {
return new ArrayList<>();
}
// 创建哈希表:Key 是排好序的字符串(特征),Value 是属于这一组的所有原字符串
Map<String, List<String>> map = new HashMap<>();
for (String s : strs) {
// 1. 寻找特征:转成字符数组、排序、再变回字符串
char[] chars = s.toCharArray();
Arrays.sort(chars);
String key = String.valueOf(chars);
// 2. 如果还没建过这个特征的格子,就先建一个
if (!map.containsKey(key)) {
map.put(key, new ArrayList<>());
}
// 3. 把原字符串 s 放进对应的格子里
map.get(key).add(s);
}
// 4. 把 Map 里所有的格子(List)打包一起返回
return new ArrayList<>(map.values());
}
}
🚀 模板二:逆向关系查找模板
1. 模板抽象与核心思想
这类题目的本质是:“一边遍历,一边回头看历史记录。” 每遇到一个新元素,先别急着继续往后走,而是去哈希表里查一下:“我一直想要的那个另一半,之前有没有出现过?”
// 通用伪代码模板
Map<数据类型, Integer> visited = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
// 1. 计算出我当前需要的“另一半”是谁
int targetValue = 目标值 – nums[i];
// 2. 回头看历史记录里有没有这个“另一半”
if (visited.containsKey(targetValue)) {
// 找到了,直接返回配对结果
return new int[]{ visited.get(targetValue), i };
}
// 3. 没找到,就把自己存进历史记录里,等后面的人来找我
visited.put(nums[i], i);
}
2. Hot 100 实战演练:LeetCode 1. 两数之和
题目大意:给定一个整数数组 nums 和一个目标值 target,找出和为目标值的那两个整数,并返回它们的数组下标。
🛠️ 纯基础款代码实现
标准无缝套用逆向查找模板,逻辑清晰直白:
class Solution {
public int[] twoSum(int[] nums, int target) {
// 创建哈希表记录历史:Key 是数字的值,Value 是这个数字在数组中的下标
Map<Integer, Integer> visited = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
// 1. 计算我需要的另一半数字是多少
int complement = target – nums[i];
// 2. 回头查历史记录:另一半数字之前出现了吗?
if (visited.containsKey(complement)) {
// 出现了!把历史数字的下标、和当前数字的下标 i 一起返回
return new int[] { visited.get(complement), i };
}
// 3. 没出现,把自己登记到历史记录里,方便后面的数字来匹配
visited.put(nums[i], i);
}
// 题目保证有解,但这行是为了编译通过
return new int[]{-1, -1};
}
}
💡 特殊衍生题型:边界查找与去重
LeetCode 128. 最长连续序列
题目大意:给定一个未排序的整数数组 nums,找出数字连续的最长序列的长度。要求算法的时间复杂度为 $O(n)$。
🛠️ 怎么用普通代码写出高效率?
这道题需要找出像 1, 2, 3, 4 这样连续的数字。我们可以用 HashSet 来做 $O(1)$ 的快速查找,并且只从每一段连续数字的“起点”开始往后数。
什么叫起点?比如有 1, 2, 3,如果哈希表里没有 0(也就是 num – 1 不存在),那 1 就是这一段的绝对起点!
class Solution {
public int longestConsecutive(int[] nums) {
// 1. 把所有数字塞进 Set 里,用来去重和做到 O(1) 速度的查找
Set<Integer> numSet = new HashSet<>();
for (int num : nums) {
numSet.add(num);
}
int longestStreak = 0; // 记录最长连续长度
// 2. 遍历我们存好的每一个数字
for (int num : numSet) {
// 【核心剪枝】:如果比当前数字小 1 的数不存在,说明当前数字是某段序列的“起点”
if (!numSet.contains(num – 1)) {
int currentNum = num; // 从起点出发
int currentStreak = 1; // 当前这段序列的长度先计为 1
// 3. 顺藤摸瓜:只要比当前数字大 1 的数还在 Set 里,就一直往后数
while (numSet.contains(currentNum + 1)) {
currentNum = currentNum + 1;
currentStreak = currentStreak + 1;
}
// 4. 数完了,和全局最长长度对比一下,谁大留谁
if (currentStreak > longestStreak) {
longestStreak = currentStreak;
}
}
}
return longestStreak;
}
}
🛠️ 总结:拿题后的“破局思考流”
拿到题目后,不要盲目写代码,先顺着这个思路想:
题目要我干什么?
-
要我分门别类 / 算频次 套用模板一。想一想什么可以当做划分格子的“特征(Key)”。
-
要我找配对 / 找组合 套用模板二。想一想当前元素的“另一半(Complement)”是谁,怎么去历史记录里捞它。
消灭双层循环:只要发现自己写了两个 for 循环嵌套,就立刻引入一个 Map 或 Set,用 O(1) 的查找把内层循环替代掉。





