【Java实习面试算法冲刺】哈希!

第1类题型:哈希表

为什么哈希表题看起来简单,你却最容易写错

很多同学第一次刷哈希表题时,会觉得这类题不难,因为经典题像 两数之和存在重复元素 看上去都不复杂。但真到了面试现场,哈希表反而是最容易暴露基本功的一类题,原因通常有这几种:

  • 你知道这题和"查找"有关,但第一反应还是双重循环。
  • 你隐约觉得要用哈希结构,却分不清该用 Map 还是 Set
  • 你会写 HashMap,但"先查再放"还是"先放再查"总是容易写反。
  • 你能把代码写出来,却说不清为什么它能把复杂度从 O(n^2) 压到 O(n)

如果你现在正处在"会做部分 Easy,但题面一变就没把握,面试一追问就容易乱"的阶段,这一类题最值得先练。读完这篇,你至少要把 3 件事练熟:看到题先想到哈希、能把模板写稳、能在面试里把思路讲清楚。


文章目录


核心知识点

哈希表最适合解决这几类问题:

  • 查某个值是否存在。
  • 统计某个元素出现次数。
  • 建立"值到位置"或"值到次数"的映射。
  • 去重。
  • 把原本需要两层循环的查找,压成一层循环。

实习面试里,你可以先这样判断:如果题目里出现"是否存在""两数配对""出现次数""去重""映射关系",优先想哈希表。

Java 里最常用的是两个结构:

  • HashMap<K, V>:需要存键值对时用。
  • HashSet<E>:只关心某个元素是否出现过时用。

最常见的写法有这几类:

java 复制代码
Map<Integer, Integer> map = new HashMap<>();
map.put(nums[i], i);
map.get(target);
map.getOrDefault(x, 0);

Set<Integer> set = new HashSet<>();
set.add(x);
set.contains(x);

边界上最容易错的是两件事:

  • 先判断还是先插入。
  • 值重复时,旧下标是否会被覆盖。

动画辅助理解:先查 complement,再写入 map

哈希表题最适合用 1. 两数之和 来建立第一印象。你可以先打开本地动画页:

哈希表:两数之和分步动画

这个动画只看一件事:遍历到当前数时,先计算它缺的另一个数,也就是 complement = target - nums[i],再去 map 里查这个 complement 是否已经出现过。

nums = [2, 7, 11, 15]target = 9 为例:

当前值 需要的 complement 查询 map 下一步
2 7 查不到 7 2 -> 0 写入 map
7 2 查到 2 -> 0 返回 [0, 1]

这一步能解释为什么代码必须"先查再放"。如果你先把当前的 7 放进 map,再去查 complement,就会混淆"之前出现过的数"和"当前正在处理的数"。在面试里,哈希表题经常不是难在 API,而是难在这个状态更新顺序。

这类题在面试里考什么

哈希表题的面试考察很基础,但非常能看出习惯是否成熟。

  • 你能不能把暴力查找压成线性时间。
  • 你是否熟悉 Java 的 Map / Set 写法。
  • 你能不能在一次遍历里同时维护状态和答案。
  • 你是否会处理重复值、缺失值、默认值这些细节。

面试官想看到的,不是你会背 HashMap 定义,而是你能快速说出:

  • 为什么不用双重循环。
  • 为什么这里用 Map 而不是 Set
  • 为什么这里应该先查再放,或者先放再查。

高频题清单

题目 来源 难度 高频属性
1. 两数之和 LeetCode 热题 100 Easy 面试高频
217. 存在重复元素 面试经典 150 Easy 面试高频
242. 有效的字母异位词 面试经典 150 Easy 基础高频
128. 最长连续序列 LeetCode 热题 100 Medium 真实面经高频

这类题最容易犯的 3 个错误

  1. 题目本质是查找和配对,结果还在写双重循环。
  2. 该用 Set 的地方用了 Map,把简单题写复杂。
  3. Map 更新顺序写反,导致同一个元素被重复使用或下标覆盖。

代表题精讲 1

题目

1. 两数之和

思路

暴力做法是双重循环,枚举每一对数,看它们的和是不是 target,复杂度是 O(n^2)

更好的办法是边遍历边查找。假设当前数是 nums[i],那我只需要看前面有没有一个数等于 target - nums[i]。这个"前面有没有"非常适合用哈希表维护。

因此做法是:

  1. 遍历数组。
  2. 先查哈希表中是否存在 target - nums[i]
  3. 如果存在,直接返回答案。
  4. 如果不存在,把当前值和当前下标放进哈希表。

这里"先查再放"非常关键,因为题目不允许同一个元素使用两次。

Java 代码

java 复制代码
import java.util.HashMap;
import java.util.Map;

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> indexMap = new HashMap<>();

        for (int i = 0; i < nums.length; i++) {
            int need = target - nums[i];
            if (indexMap.containsKey(need)) {
                return new int[] {indexMap.get(need), i};
            }
            indexMap.put(nums[i], i);
        }

        return new int[0];
    }
}

复杂度

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

如果这是面试现场,你可以这样说

这题我会先把它归类为哈希查找题。暴力做法是双重循环,时间复杂度 O(n^2)。更好的做法是一边遍历一边用 HashMap 记录已经出现过的数字及其下标。对于当前元素 nums[i],只需要看 target - nums[i] 是否已经出现过。如果出现过就能直接得到答案,这样整体时间复杂度降到 O(n)

手推一遍最容易记住的状态变化

nums = [2, 7, 11, 15]target = 9 为例:

当前下标 i 当前值 nums[i] 需要找的值 target - nums[i] 哈希表查询前内容 结果
0 2 7 {} 找不到,放入 2 -> 0
1 7 2 {2=0} 找到,返回 0, 1

这个过程最值得记住的不是答案本身,而是顺序:先查补数,再放当前元素。只要你把顺序写反,就有机会把同一个元素错误复用两次。


代表题精讲 2

题目

128. 最长连续序列

思路

这题最容易误入排序思路。排序后当然能做,但时间复杂度是 O(n log n)。题目真正想考的是:你能不能用哈希表把它压到 O(n)

关键观察是:如果一个数 x 的前一个数 x - 1 不存在,那么 x 就可能是一段连续序列的起点。只有在起点处才继续向后扩展,统计长度。

做法:

  1. 先把所有数放进 HashSet
  2. 遍历每个数 x
  3. 如果 x - 1 存在,说明它不是起点,跳过。
  4. 如果 x - 1 不存在,就从 x 开始不断检查 x + 1, x + 2...
  5. 统计最长长度。

这样每个数最多被作为扩展的一部分检查一次,总体还是线性复杂度。

Java 代码

java 复制代码
import java.util.HashSet;
import java.util.Set;

class Solution {
    public int longestConsecutive(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for (int num : nums) {
            set.add(num);
        }

        int best = 0;
        for (int num : set) {
            if (set.contains(num - 1)) {
                continue;
            }

            int current = num;
            int length = 1;
            while (set.contains(current + 1)) {
                current++;
                length++;
            }
            best = Math.max(best, length);
        }

        return best;
    }
}

复杂度

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

如果这是面试现场,你可以这样说

这题虽然也能排序做,但排序会把复杂度带到 O(n log n)。如果想做到线性时间,关键是把所有元素放进 HashSet,然后只从"可能的起点"开始往后扩展。判断起点的方法是看 num - 1 是否不存在。这样可以避免重复扫描同一段连续区间。

为什么这里用 Set 就够了

这题不需要记录下标,也不需要记录次数,真正需要的是两件事:

  • 某个数是否存在。
  • 某个数是不是一段连续区间的起点。

这两件事都只和"存在性"有关,所以 HashSet 已经足够。很多同学会下意识写成 Map<Integer, Integer>,但那只会让代码更重,并没有提供额外价值。

其余题模板与关键片段

217. 存在重复元素

核心就是去重:

java 复制代码
Set<Integer> seen = new HashSet<>();
for (int num : nums) {
    if (!seen.add(num)) {
        return true;
    }
}
return false;

242. 有效的字母异位词

如果只包含小写字母,直接用计数数组:

java 复制代码
int[] count = new int[26];
for (char c : s.toCharArray()) count[c - 'a']++;
for (char c : t.toCharArray()) count[c - 'a']--;
for (int x : count) {
    if (x != 0) return false;
}
return true;

这题的重点不是哈希一定比数组好,而是你要知道在字符范围固定时,数组比 Map<Character, Integer> 更直接。


边界、易混点与替代方案

MapSet 怎么选

  • 只关心元素是否出现过:优先 Set
  • 还要记录下标、次数、映射关系:优先 Map

如果你发现自己在代码里存了一个值,却从头到尾没有用到这个值对应的附加信息,通常就说明结构选重了。

哈希和排序怎么选

很多题都能"排序后再做",但排序会改变复杂度,也可能打乱原始下标信息。像 两数之和 这类要保留下标的题,哈希往往更直接;像 最长连续序列 这种题,排序可做但不是题目真正想考的最优思路。

哈希和计数数组怎么选

如果值域或字符集固定,计数数组常常比哈希更轻,比如 有效的字母异位词。如果值域不固定、需要通用映射能力,哈希就更稳。

哪些情况要格外小心

  • 题目不允许同一个元素用两次时,重点检查"先查后放"。
  • 题目有重复值时,重点检查旧值被覆盖后是否影响答案。
  • 题目只问存在性时,别把简单题写成维护复杂映射的大题。

你学完后怎么验证自己真的会了

不要只看完文章就算结束。更有效的做法,是用下面 3 组自测来判断你是否真的掌握:

  1. 10 分钟内独立写出 两数之和,并且不用回忆题解文字,只靠"先查再放"的识别信号完成。
  2. 看见 最长连续序列 时,能主动说出为什么不是必须排序,为什么只从起点开始扩展。
  3. 看到 存在重复元素有效的字母异位词 这类题时,能快速判断该用 SetMap 还是计数数组。

如果你在自测时出现下面任意一种情况,就说明还没有真正掌握:

  • 能看懂代码,但自己从空白开始写不出来。
  • 写得出来,但说不清为什么复杂度是 O(n)
  • 知道要用哈希,但结构一会儿用 Map,一会儿用 Set,没有判断标准。

比较稳妥的过关标准是:你能在 15 分钟内做出一道基础哈希题,并且在 1 分钟内口述题型判断、核心思路和复杂度。

错题本记录方式

哈希表题建议重点记录这几类卡点:

  • 我到底为什么没想到用哈希,而是写了暴力。
  • 我是否判断错了应该用 Map 还是 Set
  • 我是"先查后放"写错,还是默认值更新写错。
  • 这题的识别信号是什么,比如"配对""频次""去重""是否存在"。

适用范围与边界

  • 本文默认你已经会 Java 数组遍历、条件判断和基础集合写法。
  • 本文重点服务于 Java 实习面试、LeetCode 高频 Easy / Medium 和常见笔试基础题。
  • 本文不覆盖哈希冲突底层实现、并发哈希容器或竞赛向复杂技巧。

如果你当前连 HashMap / HashSet 的基本 API 都不熟,建议先补 Java 集合基础,再回来刷这类题,效果会更好。

面试前 3 分钟速记

  • 看到"配对、是否存在、频次、去重",优先想哈希表。
  • 只关心是否出现过,用 HashSet
  • 还要记下标或次数,用 HashMap
  • 两数之和 记住"先查再放"。
  • 最长连续序列 记住"只从起点开始扩展"。
  • Java 常用写法:containsKeygetOrDefaultadd 返回值。

结尾:把"会做题"变成"会识别、会写、会讲"

哈希表之所以适合作为第一类题型,不是因为它最简单,而是因为它最适合帮你建立题型意识。你从这类题开始练,最重要的收获不是记住某道题的答案,而是养成几个稳定动作:

  • 看到题先判断是不是"存在性、配对、频次、去重"问题。
  • 写代码时先想清楚 MapSet 的职责区别。
  • 做完题后,能用一句话解释为什么哈希把暴力查找压成了线性复杂度。

如果这几个动作稳定下来,后面的双指针、滑动窗口、图论和动态规划,你都会更容易进入状态。


感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!

下一篇继续看第 2 类高频题型:->->->【Java实习面试算法冲刺】双指针<-<-<-

相关推荐
大耳朵糊涂1 小时前
找前/后驱节点
算法
孤狼warrior1 小时前
从冒泡到传送带流水线:一个3D沉浸式算法靶场,让思想的伟力改变世界
python·算法·typescript
带刺的坐椅1 小时前
ReActAgent 使用指南:构建会思考、能行动的 AI Agent
java·ai·llm·solon·loop·react-agent
bu_shuo2 小时前
计算机二级学习-查找和排序
学习·算法·排序算法
漂亮的摩托2 小时前
如何编写一个SpringBoot项目告警推送的Starter
java·spring boot·后端
盖伦暴打诺手2 小时前
类比推理知识点
java
ch.ju2 小时前
Java程序设计(第3版)第四章——类加载
java·开发语言
河阿里2 小时前
SLF4J深度指南(Java):从原理到 Spring 项目实战
java·开发语言·spring
小沈同学呀2 小时前
飞书机器人+Spring AI Function Calling实战-扔掉MCP Client让LLM直接操控工具
java·开发语言·functioncalling·spring ai·飞书机器人