LeetCode 1. 两数之和 | 从暴力枚举到哈希表最优解 + 空间换时间思想全复盘

一、题目与核心考点

题目要求

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案,且同一个元素不能使用两次。你可以按任意顺序返回答案。

核心考点

这道题本质考察「查找效率的优化」,核心能力点:

  1. 暴力枚举的基础遍历逻辑,避免重复配对
  2. 哈希表的 O (1) 查找特性,实现空间换时间
  3. 边界防御与遍历顺序控制,保证逻辑严谨

最优解时间复杂度 O (n),空间复杂度 O (n),是典型的空间换时间方案。


二、思路完整推导:从暴力到最优

1. 入门思路:暴力枚举两两配对

最直观的解法非常容易想到:遍历数组中所有可能的数对,判断它们的和是否等于 target。

  • 外层循环固定第一个数的下标 i
  • 内层循环从 i+1 开始,枚举第二个数的下标 j
  • 逐一判断 nums[i] + nums[j] == target,命中则返回下标

内层循环从 i+1 开始,有两个好处:

  1. 不会出现 (i,j)(j,i) 重复判断同一对数
  2. 不会用到同一个下标的元素两次,符合题目要求

但暴力法有一个明显的缺点:双重循环嵌套,时间复杂度是 O (n²),数组规模较大时会严重超时。

2. 发现瓶颈:查找另一半的效率太低

暴力法慢的根源是什么? 对于每一个 nums[i],我们要找的其实是 target - nums[i] 这个「差值」有没有在数组里出现过。暴力法用了一次循环去查找这个差值,每次查找都是 O (n),所以整体才会是 O (n²)。

那么问题就转化成了:能不能用更快的方式,查到「某个值有没有出现过、它的下标是多少」?

答案就是哈希表。哈希表的查找操作平均时间复杂度是 O (1),刚好完美解决查找慢的问题。

3. 最优解:哈希表一次遍历

核心思路:遍历数组的同时,用哈希表记录已经走过的元素,边遍历边查找

  1. 维护一个哈希表,key 存数值,value 存该数值对应的下标
  2. 遍历到当前元素 nums[i] 时,计算需要的差值 diff = target - nums[i]
  3. 去哈希表里查这个差值是否存在:
    • 存在:说明之前已经遇到过这个差值,两个数刚好配对,直接返回两个下标
    • 不存在:把当前数值和下标存入哈希表,继续往后遍历

全程只需要一次遍历,每次查找和插入都是 O (1),整体时间复杂度降到 O (n)。

4. 关键细节:先查再存,避免自匹配

这里有一个非常重要的顺序:先判断差值是否存在,再把当前元素存入哈希表。 如果反过来先存再查,当前元素会被自己匹配到 ------ 比如 target=4,当前元素是 2,刚存进去就查到了自己,返回了两个相同的下标,不符合题目要求。

先查再存的话,哈希表里永远只有「当前元素之前的所有元素」,绝对不会出现自己和自己配对的情况,逻辑天然严谨。


三、两种解法完整代码

解法一:暴力枚举法(入门理解)

java 复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];
        // 双重循环枚举所有数对
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] + nums[j] == target) {
                    res[0] = i;
                    res[1] = j;
                    return res; // 找到直接返回
                }
            }
        }
        return res;
    }
}
  • 时间复杂度:O (n²)
  • 空间复杂度:O (1)

解法二:哈希表一次遍历(面试最优解)

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

class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 边界防御
        if (nums == null || nums.length < 2) {
            return new int[]{-1, -1};
        }

        Map<Integer, Integer> map = new HashMap<>();
        // 一次遍历,边查边存
        for (int i = 0; i < nums.length; i++) {
            int diff = target - nums[i];
            // 检查差值是否在之前出现过
            if (map.containsKey(diff)) {
                return new int[]{map.get(diff), i};
            }
            // 没找到就存入当前元素
            map.put(nums[i], i);
        }
        return new int[]{-1, -1};
    }
}
  • 时间复杂度:O (n)
  • 空间复杂度:O (n)

四、逐行代码深度拆解(哈希表版)

1. 边界防御

复制代码
if (nums == null || nums.length < 2) {
    return new int[]{-1, -1};
}
  • nums == null:防止空指针异常,保证数组对象存在
  • nums.length < 2:数组不足两个元素,不可能组成数对,直接返回无效下标
  • 短路或顺序不能反,先判引用再判属性

2. 哈希表初始化

复制代码
Map<Integer, Integer> map = new HashMap<>();

哈希表的键值设计:

  • key:数组元素的数值,用于快速查找
  • value:该数值对应的数组下标,用于返回结果

3. 单次遍历核心逻辑

复制代码
int diff = target - nums[i];
if (map.containsKey(diff)) {
    return new int[]{map.get(diff), i};
}
map.put(nums[i], i);

核心三步:

  1. 算差值:当前元素需要配对的另一个数是多少
  2. 查历史:去哈希表里找这个差值有没有出现过
  3. 存当前:没找到就把自己存进去,供后面的元素匹配

严格遵循「先查后存」的顺序,保证不会自匹配。


五、新手必踩坑清单

坑 1:数组只声明不初始化

  • 错误写法:int[] res; res[0] = -1;
  • 后果:空指针异常,只声明引用没创建数组对象
  • 修正:必须用 new int[2] 初始化数组

坑 2:找到答案不立即返回

  • 后果:做大量无用遍历,性能浪费;多解场景下可能覆盖正确结果
  • 原则:算法题找到答案立刻返回,避免多余运算

坑 3:先存后查,顺序颠倒

  • 后果:元素自己匹配自己,返回相同下标
  • 原则:永远先查差值,再存入当前元素

坑 4:内层循环从 0 开始

  • 错误写法:for(int j = 0; ...)
  • 后果:同一对数判断两次,且可能出现 i=j 的自匹配
  • 原则:暴力法内层循环必须从 i+1 开始

坑 5:哈希表键值搞反

  • 错误:key 存下标,value 存数值
  • 后果:无法通过数值快速查找,失去哈希表的意义
  • 原则:用「要查找的内容」当 key,这里要按数值查找,所以数值做 key

六、面试相关

口述思路(直接背)

这道题我用哈希表来实现,采用空间换时间的思路。

我遍历数组,对于每个元素,计算它与目标值的差值,然后去哈希表中查找这个差值是否存在。如果存在,说明找到了两个数,直接返回两个下标;如果不存在,就把当前元素和它的下标存入哈希表。全程只需要一次遍历,查找操作是 O (1),整体时间复杂度 O (n),空间复杂度 O (n)。

这里要注意先查找再存入,避免同一个元素被使用两次。

高频追问

  1. 为什么不用排序 + 双指针? 因为题目要求返回原始下标,排序会打乱元素的原始位置。如果要保留下标需要额外存储,实现起来比哈希表更复杂,效率也没有优势。如果题目只需要判断是否存在、不需要返回下标,双指针是可行的。
  2. 如果数组中有重复元素怎么办? 不影响。题目保证只有唯一解,先出现的元素会先存入哈希表,后出现的元素匹配到就直接返回,不会出错。
  3. 暴力法和哈希法怎么选? 数组规模小时暴力法也能跑,但工程和面试中优先选哈希表,时间复杂度更优。如果严格限制空间复杂度,再考虑暴力法。
  4. 这道题的延伸题有哪些?
    • 三数之和、四数之和:排序 + 双指针
    • 两数之和 II(有序数组):双指针
    • 两数之和 III(数据结构设计):哈希表设计

七、复习速记口诀

两数之和不用蛮,哈希查表省时间; 先算差值查历史,再存当前防自连; 暴力双重循环慢,空间换效是关键。


总结

两数之和作为算法入门第一题,价值不在于代码本身,而在于它引入了「空间换时间」的核心算法思想,以及哈希表在查找类问题中的强大作用。 从暴力双重循环到哈希表一次遍历,是非常经典的算法优化路径 ------ 找到耗时的瓶颈操作,用合适的数据结构把它的时间复杂度降下来,这也是解决绝大多数算法题的通用思考方式。 掌握这个思路后,后续的三数之和、四数之和等同类型题目,都可以快速迁移解法。

相关推荐
happyprince1 天前
01-整体观-Codex全貌解读(源码)
算法·ai编程
俊基科技1 天前
别自己写算法了,让这颗37.5毫米的模组替你“闭嘴干活”
神经网络·算法·硬件开发·ai降噪·语音模块·回音消除
_Narcissus_1 天前
动态规划初探(含完整代码实现)
c语言·数据结构·c++·算法·动态规划·背包问题·最短路径
世人万千丶1 天前
鸿蒙项目实战 - 社区活动编排板:标签云布局算法与自动换行
学习·算法·华为·harmonyos·鸿蒙
zander2581 天前
LeetCode 279. 完全平方数
算法·深度优先
典典分享指南1 天前
VS Code Git 工作树:解锁多分支并行开发新体验
算法·决策树·逻辑回归·启发式算法
_Narcissus_1 天前
枚举和模拟算法笔记
c语言·数据结构·c++·笔记·算法·模拟·枚举
天疆说1 天前
策略的进化:从随心所欲到稳健前行
算法
冻柠檬飞冰走茶1 天前
《数据结构实验指导-C++语言版》 在顺序表 list 中查找元素 x
开发语言·数据结构·c++·算法·list