打开 LeetCode 的 Hash Table 标签,你会发现题量不少,但真正痛苦的往往不是题解看不懂,而是很多题目你跟着题解走觉得“就这?”,合上答案自己写又卡在同一个地方:到底什么时候该用哈希表?用哈希表的时候,Key 存什么?Value 又该存什么?如果你也卡在这几个问题上,这篇内容应该能帮你把哈希章节的“思路”和“实现”串成一条线。
这篇文章面向正在按 LeetCode 标签顺序刷哈希表题目的人,也适合准备面试前想重新整理查找、去重、计数、记忆化这几类问题的朋友。我会从暴力解的推导过程讲起,然后按题型给出可以直接复用的代码模板,再深入哈希设计题和一些进阶用法,比如“目标和”这道题里藏在 DFS 底下的状态哈希。所有代码都用 Java 写,换成 C++ 的 unordered_map 或 Python 的 dict 思路完全一样。
1. 哈希表在算法题里的真正角色:不是容器,是“历史检索”
1.1 哈希题大多可以浓缩成一个动作:查之前见过的信息
很多人以为哈希表这类题的核心是“会用 HashMap 这个 API”,其实不对。哈希表在算法题里几乎只有三类用途:去重、计数、关联映射。
去重解决的是“这个东西之前出现过吗”,比如判断字符串里有没有重复字符;计数解决的是“这个东西之前出现过几次”,比如统计每个词的频率;关联映射则是“这个东西之前对应的值是什么”,比如两数之和里需要根据差值找下标,又比如记忆化搜索里根据状态找已经算过的答案。
这三类用途本质上都有一个共同点:你在遍历数据的过程中,需要频繁回头查看历史信息。
如果题目里没有“回头查”的动作,哈希表大概率不是最优解。如果题目里有“回头查”的动作,那哈希表就可以列入候选方案。这个判断标准虽然简单,但特别管用。说实话,很多人哈希章节卡住,不是不会写 put 和 get,而是根本没想到“这个题需要把遍历过的信息留下来”,所以才会在双层循环里白白浪费时间。
1.2 数字题里不是每次都需要 HashMap,数组就是最原始的哈希表
哈希表章节中有一类题其实可以用数组完成,而且更快,比如“有效的字母异位词”。
这道题判断两个字符串的字母组成是否完全相同。如果只用小写字母,键的取值范围一共就 26 个。你当然可以写一个 HashMap<Character, Integer> 统计频次,但更克制的做法是开一个 int[26],用 s.charAt(i) - 'a' 作为下标。
数组在这里本质上就是一个哈希表:把键通过一个固定函数映射到下标,而且这个映射是完美无冲突的,因为每个字符都对应一个连续的数。
在你决定用 HashMap 之前,先问一句:键的值域是不是连续且有限的?如果是,数组往往更合适。因为 HashMap 虽然查询是 O(1),但这个 O(1) 背后有哈希计算、桶定位、处理哈希冲突的成本;数组则直接通过内存地址访问,完全没有这些额外开销。刷题时我经常看到有人对所有计数类问题一律上 HashMap,明明开一个数组就能解决,非要付出好几倍的常数时间,这种做法在数据量大时是很吃亏的。
1.3 真正需要 HashMap 的时候:键稀疏、值域大、对象复杂
反过来,什么时候必须用 HashMap?键的值域很大但实际出现的键很少,或者键本身是一个字符串、数组、坐标这类复杂对象。
比如“两数之和”里,数组元素的值可能达到 10^9,你不可能开一个长度为 10^9 的数组去记录每个元素的下标。再比如“字母异位词分组”里,你需要把字符串本身或它的某种变换形式作为键,这时候数组下标就无能为力了。
一个有意思的类比是:数组相当于你预先租下整个写字楼的每一间办公室,只要你明确知道要用哪些房间,这种方案简单高效;HashMap 则相当于你只有一张写着房间号和入住人信息的清单,房间再多也不怕,因为你可以按清单找,但每次找人时都要先算一下房间号,再走过去。
所以哈希章节一个很重要的意识是:并不是要用 Map 才是“使用哈希”,数组下标同样利用了哈希思想,而且可能更优。真正的选择标准是键空间的大小和稀疏程度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从“双重循环”到“哈希”的推理路径,两数之和就是最好教材
2.1 暴力解法的性能瓶颈:一次遍历只能带一个变量,另一个全靠重新扫描
两数之和是哈希章节最经典的起点。题目描述很简单:在数组里找两个数,使它们的和等于 target,返回下标。
很多人看到题的第一反应是双层循环:
java复制for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
这段代码没有任何问题,时间复杂度 O(n^2)。问题出在哪里?外层每走到一个数,内层都要把后面所有数字重新扫描一遍,本质上是在不停地做“查找”。而查找这个操作,恰恰是哈希表最擅长的事情。
这里我想强调一个以后刷所有题都用得上的思维:当你发现一个暴力解法里反复出现“扫描剩余部分寻找目标值”的逻辑,就应该警惕了——那段逻辑很可能可以用一个哈希结构替代,把每次 O(n) 的查找变成 O(1) 的哈希查询。
2.2 边遍历边存哈希,为什么不会匹配到自己
哈希版本的两数之和只需要一趟遍历:
java复制public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i];
if (map.containsKey(need)) {
return new int[]{map.get(need), i};
}
map.put(nums[i], i);
}
return new int[0];
}
最关键的一行是:先查,再 put。
如果把顺序交换,先 map.put(nums[i], i) 再查询 need,那么当 nums[i] 恰好等于 need 时,你会查到当前元素自己的下标。比如 nums = [3,3]、target = 6,遍历到第二个 3 时,如果先 put,map 里保存的是 3 -> 1,再查 need 得到下标 1,于是返回 [1,1],同一个元素被用了两次,答案错误。
边
