Hot 100 刷题笔记 · 数组与哈希专题(3 题)
收录:1. 两数之和、238. 除自身以外数组的乘积、41. 缺失的第一个正数;每题按 ① 分析思路 → ② 解法讲解 → ③ 知识点总结 组织。
一、1. 两数之和

① 题目分析 & 解题思路
给定 nums 与 target,找两个相加等于 target 的元素并返回下标;不能重复使用同一元素,保证只有一组答案。
- 暴力:两层循环枚举组合判断和,能通过但不满足进阶。
- 哈希表(优化) :找互补数
complement = target - nums[i];哈希表存【数值→下标】,先查互补数,命中即返回下标,否则存入当前数与下标。
② 常见解法及讲解
解法 1:暴力双循环(O(n2)O(n^2)O(n2),仅用于理解)
java
class Solution {
public int[] twoSum(int[] nums, int target) {
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};
}
}
}
return new int[0];
}
}
好理解、不占额外空间;大数据下慢,非最优解。
解法 2:HashMap 哈希表(最优,面试首选)
java
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
// HashMap 哈希表 (数值,下标)
//// 先看互补数是否已经存在map中
return new int[] { map.get(complement), i };
}
// 不存在,把当前数和下标放入map
map.put(nums[i], i);
}
return new int[0];
}
}
推演 nums=[3,2,4], target=6:i=0 互补数 3 不在表中 → put(3,0);i=1 互补数 4 不在 → put(2,1);i=2 互补数 2 在表中(下标 1)→ 返回 [1,2]。
先查再 put ,避免自己匹配自己([3,3] target=6:先存入第一个 3,第二个 3 查到它,返回 0,1)。
复杂度总结
- 解法 1(暴力双循环):时间 O(n2)O(n^2)O(n2),空间 O(1)O(1)O(1);
- 解法 2(HashMap):时间 O(n)O(n)O(n),空间 O(n)O(n)O(n)。
③ 知识点总结
常见易错变种:
- j 从 0 开始并写
i==j判断,容易重复使用同一元素; - 返回
{nums[i], nums[j]}(返回数值而非下标); - 忘记 return,或返回 null。
补充:不推荐排序 + 双指针------排序打乱下标,需额外保存原始下标;该法适合返回值而非下标的题目。
二、238. 除自身以外数组的乘积

① 题目分析 & 解题思路
返回 answer,answer[i] = 除 nums[i] 外所有元素的乘积; 不能用除法(含 0 时总乘积 / numsi 会出错),⏱️ 时间 O(n)O(n)O(n),进阶额外空间 O(1)O(1)O(1)(输出数组不计)。
前缀积 + 后缀积 :prefix[i] 为 i 左边元素乘积,suffix[i] 为 i 右边元素乘积,answer[i] = prefix[i] × suffix[i]。进阶:复用输出数组存前缀,用变量滚动记录后缀,从后往前相乘,不开 suffix 数组。
② 常见解法及讲解
解法 1:前缀 + 后缀数组(空间 O(n)O(n)O(n))
java
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] prefix = new int[n];
int[] suffix = new int[n];
int[] ans = new int[n];
// 前缀:prefix[i] = i左边所有乘积
// 原数组里第一个的前缀记为1
prefix[0] = 1;
// 从前往后累积
for (int i = 1; i < n; i++) {
prefix[i] = prefix[i - 1] * nums[i - 1];
}
// 后缀:suffix[i] = i右边所有乘积
// 原数组最后一个的后缀记为1
suffix[n - 1] = 1;
// 从后往前累积
for (int j = n - 2; j >= 0; j--) {
suffix[j] = suffix[j + 1] * nums[j + 1];
}
// 左右相乘
for (int k = 0; k < n; k++) {
ans[k] = prefix[k] * suffix[k];
}
return ans;
}
}
逻辑直观、不易写错;缺点是开两个辅助数组,空间 O(n)O(n)O(n)。
解法 2:进阶版 O(1)O(1)O(1) 额外空间(面试优先写这个)
java
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
// 第一步:ans数组先存前缀乘积
ans[0] = 1;
for (int i = 1; i < n; i++) {
ans[i] = ans[i - 1] * nums[i - 1];
}
// 第二步:从后往前,用一个变量保存后缀乘积,直接乘进ans
int suffix = 1;
for (int j = n - 1; j >= 0; j--) {
ans[j] = ans[j] * suffix;
suffix = suffix * nums[j];// 更新后缀,给左边下一个用
}
return ans;
}
}
推演 [1,2,3,4]:前缀遍历后 ans=[1,1,2,6],suffix 数组 [24,12,4,1];从后往前:i=3 ans=6×1=6、suffix=4;i=2 ans=2×4=8、suffix=12;i=1 ans=1×12=12、suffix=24;i=0 ans=1×24=24,结果 [24,12,8,6]。
复杂度总结
- 解法 1(前后缀数组):时间 O(n)O(n)O(n),空间 O(n)O(n)O(n);
- 解法 2(进阶版):时间 O(n)O(n)O(n),空间 O(1)O(1)O(1)(不计输出数组)。
③ 知识点总结
易错点:
- 前缀第一个、后缀最后一个初始值是 1,不是 0;
- 前缀用
nums[i-1]、后缀用nums[i+1],下标容易写反; - 不能用除法,遇到 0 的样例除法方案直接失效;
- 数组长度最大 10510^5105,暴力双重循环 O(n2)O(n^2)O(n2) 会直接超时。
三、41. 缺失的第一个正数

① 题目分析 & 解题思路
给无序整数数组,找未出现的最小正整数 ;时间 O(n)O(n)O(n)、额外空间 O(1)O(1)O(1),不能开哈希表。
关键结论 :长度为 n,答案必在 1,n+11, n+11,n+1:1~n 全出现则返回 n+1,否则返回最小缺失数;负数、0、大于 n 的数可忽略。
原地哈希 (把数组本身当哈希表):合法正数 xxx(1≤x≤n1\le x\le n1≤x≤n)放到下标 x−1x-1x−1(3→2,1→0)。流程:① 遍历交换归位;② 再遍历,第一个不满足 nums[i]==i+1 的位置,i+1 即答案;③ 全部满足返回 n+1。
② 常见解法及讲解
解法 1:原地哈希(推荐,面试标准,O(n)O(n)O(n) 时间、O(1)O(1)O(1) 空间)
java
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
// nums[i]值 的目标位置上的值是 nums[nums[i]-1]
// 满足三个条件才交换:是合法正数、不在正确位置、目标位置的值和它不一样
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
// 交换 nums[i] 和 nums[nums[i]-1]
int temp = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = temp;
}
}
// 找第一个 nums[i] != i+1
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
// 1~n全都有
return n + 1;
}
}
推演 nums=[3,4,-1,1],n=4:i=0,3 合法,交换下标 0、2 → [-1,4,3,1],-1 退出 while;i=1,4 合法,交换下标 1、3 → [-1,1,3,4],1 继续交换到下标 0 → [1,-1,3,4];i=2 的 3、i=3 的 4 已就位。第二轮:i=0 的 1==1 ✔,i=1 的 -1≠2 → 返回 2 ✔。
解法 2:标记法(原地哈希另一种实现,不交换)
先把负数、0 改成 n+1;再把下标 x-1 的值改成负数标记;最后找第一个正数下标。
java
class Solution {
public int firstMissingPositive(int[] nums) {
int n = nums.length;
// 第一步:把<=0的数都改成n+1
for (int i = 0; i < n; i++) {
if (nums[i] <= 0) nums[i] = n + 1;
}
// 第二步:标记
for (int i = 0; i < n; i++) {
int x = Math.abs(nums[i]);
if (x <= n) {//限定范围<=n里的整数,结合上一步排除了<=0的值
// 把下标x-1的值nums[x - 1] 都改成负数做标记
nums[x - 1] = -Math.abs(nums[x - 1]);
}
}
// 第三步:找第一个正数
for (int i = 0; i < n; i++) {
// nums[i] > 0 说明值(i+1)不属于原数组里的正数,nums[i]没在上一步被改为负数
if (nums[i] > 0) return i + 1;
}
return n + 1;
}
}
不用 while 交换、不易死循环;缺点是理解稍绕。
解法 3:HashSet(简单但不满足空间要求,适合初学理解)
java
class Solution {
public int firstMissingPositive(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int x : nums) set.add(x);
int res = 1;
// 从正数1开始依次判断出第一个不在原数组里的正数
while (set.contains(res)) {
res++;
}
return res;
}
}
好写好理解;空间 O(n)O(n)O(n),面试会被追问优化。
复杂度总结
- 原地交换 / 标记法:O(n)O(n)O(n) 时间、O(1)O(1)O(1) 额外空间;
- HashSet:O(n)O(n)O(n) 时间、O(n)O(n)O(n) 空间。
③ 知识点总结
常见出错点:
- 只交换一次不用 while:
[3,4,-1,1]中 3 归位后,该位置的 -1 还要处理; - 不判断
nums[i]==nums[nums[i]-1]会死循环(重复数字); - 漏判
nums[i]<=n,大于 n 的数不应交换; - 下标换算错:x 对应下标是 x-1,不能直接用 x;
- 让负数、0 也参与交换;
- 用 HashSet 空间 O(n)O(n)O(n),不满足进阶要求。