LeetCode 1. 两数之和:为什么 HashMap 能一趟解决问题?

12 阅读4分钟

一、题目回顾

给定一个整数数组 nums 和一个目标值 target
在数组中找出 两个数,使它们的和等于 target,并返回它们的 下标

要求:

  • 每个输入只会有一个解
  • 同一个元素不能使用两次

示例:

nums = [2,7,11,15], target = 9
返回 [0,1]

二、最直观的思路:暴力枚举

最容易想到的办法是:
两层循环,枚举所有的数对

for (int i = 0; i < n; i++) {
    for (int j = i + 1; j < n; j++) {
        if (nums[i] + nums[j] == target) {
            return new int[]{i, j};
        }
    }
}

时间复杂度:O(n²)

问题在于:

  • 每多一个元素,就多做大量无意义的比较
  • 当 n 很大时,性能会迅速崩掉

所以关键问题是:

能不能在遍历数组的同时,快速判断
“有没有一个数,刚好能和当前数凑成 target”?


三、核心思想:把「找人」变成 O(1)

假设我们当前遍历到 nums[i]
想知道是否存在一个数 x,满足:

x + nums[i] = target

等价于:

x = target - nums[i]

所以问题就变成了:

我们之前见过 target - nums[i] 吗?

这一步如果每次都遍历数组去找,还是 O(n)
但如果我们用一个 HashMap 记录已经遍历过的元素:

  • key:数值
  • value:下标

那么查询就可以做到 O(1)


四、一步一步拆解 HashMap 解法

标准代码:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        HashMap<Integer,Integer> mp = new HashMap<>();
        mp.put(nums[0], 0);

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

下面逐行来看它在做什么,以及为什么这么写


1. HashMap 里存的是什么?

HashMap<Integer,Integer> mp = new HashMap<>();

这里的含义是:

  • key:数组中的某个数值
  • value:这个数值对应的下标

也就是说:
mp 记录的是「已经遍历过的元素」


2. 为什么先放 nums[0]?

mp.put(nums[0], 0);

这是一个常见但容易被忽略的细节。

代码的整体策略是:

从左到右遍历数组
每次先“查找配对”,再“加入当前数”

所以在进入 for 循环前:

  • 我们需要保证 HashMap 中 已经有一个元素
  • 这样从 i = 1 开始,就可以直接做判断

当然,也可以把这一步写进循环中(从 i = 0 开始),
这只是实现方式不同,思想完全一致。


3. 核心判断逻辑

if (mp.containsKey(target - nums[i])) {
    return new int[]{mp.get(target - nums[i]), i};
}

这是整道题的灵魂。

含义是:

  • 当前数是 nums[i]

  • 想找的另一个数是 target - nums[i]

  • 如果它已经出现在 HashMap 中

    • 说明之前某个下标 j
    • 满足 nums[j] + nums[i] = target

此时直接返回:

[j, i]

注意:

  • j < i,保证不会重复使用同一个元素
  • 顺序天然正确,不需要额外处理

4. 为什么找不到时才 put?

else {
    mp.put(nums[i], i);
}

这是一个非常关键的设计点。

原因只有一个:

不能用同一个元素两次

如果我们先 put,再 containsKey
那么当:

nums[i] * 2 == target

时,就可能错误地把同一个下标用两次。

而现在的顺序是:

  1. 只在「之前的元素」中找配对
  2. 当前元素只作为“未来的候选人”

逻辑上非常干净。


五、用一个例子走完整个过程

示例:

nums = [2, 7, 11, 15]
target = 9

初始化:

mp = {2 -> 0}

i = 1,nums[1] = 7

  • target - 7 = 2
  • mp.containsKey(2) == true

直接返回:

[0, 1]

算法结束。


六、复杂度分析

  • 时间复杂度:O(n)

    • 每个元素最多被 put 一次
    • HashMap 查询是常数级
  • 空间复杂度:O(n)

    • 最坏情况下,所有元素都被存入 HashMap

这是当前问题的最优解法。


七、总结

这道题的本质不是「两数相加」,而是:

用空间换时间,把“查找”从 O(n) 降到 O(1)

关键点只有三个:

  1. 用 HashMap 记录「已经遍历过的数」
  2. 每次先找 target - nums[i]
  3. 再把当前数放进 Map

理解了这套思路后,
你会发现它会反复出现在:

  • 两数之和
  • 三数之和(作为子步骤)
  • 各类“配对 / 补数”问题中

不是模板,而是一种可以迁移的思维方式