两数之和:从暴力到优雅的哈希表之旅

94 阅读6分钟

引言

“面试官最怕什么?不是你不会做题,而是你只会背答案。”
—— 来自一位大厂算法面试官的真实吐槽

今天我们要聊的,是 LeetCode 上最经典、也最“有毒”的题目之一:1. 两数之和(Two Sum)** 。别看它标着“简单”,但它是无数人进入大厂的第一道门槛,也是算法思维启蒙的起点。

在这篇文章中,我们将:

  • 深入解析这道题的核心逻辑
  • 逐行解读代码,添加详细注释
  • 全面讲解哈希表(Hash Map) 的工作原理
  • 揭秘大厂面试官真正想考察的是什么
  • 给你一套高分表达策略,让你在面试中脱颖而出!

题目回顾:两数之和

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

示例:

输入: nums = [2,7,11,15], target = 9
输出: [0,1]
解释: nums[0] + nums[1] == 9

看起来很简单?没错,但正是这种“简单题”,最能暴露你的算法思维深度


解法对比:暴力 vs 哈希表

暴力解法:双重循环(O(n²))

for (let i = 0; i < nums.length; i++) {
  for (let j = i + 1; j < nums.length; j++) {
    if (nums[i] + nums[j] === target) {
      return [i, j];
    }
  }
}

这个方法虽然直观,但时间复杂度是 O(n²) ,当数组很大时,性能会急剧下降。比如有 1000 个元素,就要比较近百万次!😱

优化解法:哈希表(O(n))

这才是真正的“大厂风格”——用空间换时间,把查找效率提升到极致!

我们来看这段原汁原味的代码,并在每一行都加上详细的解析:

function twoSum(nums, target) {
  // nums是一个整数数组,每个元素都不同
  // target是一个整数,表示目标值
  // es6 提供了hashMap 很重要 
  // 语言都提供了一些内置的数据结构 数组[] 哈希表{}

  // diffs 哈希表 用来存储数组中的元素和它们的索引
  const diffs = new Map();  // es6 没有hashMap  O(1)时间复杂度
  const len = nums.length; // 缓存数组的长度

  // 遍历数组
  for (let i = 0; i < len; i++) {
    // 计算当前元素的补数
    const complement = target - nums[i]; // 和变成差

    // 检查补数是否已存在于哈希表中
    if (diffs.has(complement)) {
      // 如果存在,则返回补数的索引和当前元素的索引
      return [diffs.get(complement), i];
    }

    // 如果不存在,则将当前元素和它的索引存入哈希表
    diffs.set(nums[i], i);
    // diffs[nums[i]] = i;  // 这样写也可以,但不推荐,因为对象键名限制
  }
}

console.log(twoSum([2, 7, 11, 15], 9));

逐句解析:代码背后的智慧

让我们一行一行拆解这段代码,看看它到底做了什么。

第一步:定义变量

const diffs = new Map();

👉 这里使用了 ES6 的 Map 对象作为哈希表。为什么不用普通对象 {}

  • 因为 Map 支持任意类型的键(包括对象、函数等),而普通对象只能用字符串或符号作键。
  • 更重要的是,Mapset()get() 方法更清晰,且性能稳定。
  • 而且 Map 是专门设计来模拟“哈希表”行为的,更适合算法题。
const len = nums.length;

👉 缓存数组长度,避免每次循环都调用 .length,虽然现代引擎已经优化了这一点,但这是一个良好的编程习惯。


第二步:遍历数组

for (let i = 0; i < len; i++) {

👉 单次遍历,这是关键!我们不再需要嵌套循环了。


第三步:计算“补数”

const complement = target - nums[i]; // 和变成差

🎯 这是整个算法的灵魂所在!

把“找两个数加起来等于 target” → 转化成“找一个数等于 target 减去当前数”。

举个例子:
如果 target = 9,当前 nums[i] = 2,那么我们需要找的是 9 - 2 = 7

如果我们之前见过 7,那就可以直接返回了!

这就是所谓的 “求和变求差” ,是算法思维的一大跃迁。


第四步:检查补数是否存在

if (diffs.has(complement)) {
  return [diffs.get(complement), i];
}

👉 has() 方法判断某个键是否存在,时间复杂度是 O(1)

👉 get() 获取对应键的值(也就是索引)。

✅ 找到了就立刻返回结果,不需要继续遍历。


第五步:存入哈希表

diffs.set(nums[i], i);

👉 把当前数字和它的索引存进哈希表,等待后续匹配。

⚠️ 注意顺序:先查后存

为什么不能先存再查?
因为如果先存,可能会出现自己跟自己配对的情况(比如 [3,3],target=6),但我们要求不能重复使用同一个元素。

所以必须是:先查有没有补数,再把自己存进去


哈希表:数据结构界的“超级英雄”

什么是哈希表?

哈希表(Hash Table)是一种基于键值对(key-value) 的数据结构,支持快速插入、删除和查找操作。

它的工作原理如下:

  1. 输入一个“键”(key)
  2. 通过一个叫做“哈希函数”(hash function)的算法,把这个键转换成一个唯一的索引
  3. 将对应的“值”(value)放在这个索引位置上
  4. 查找时,再次用哈希函数定位,就能瞬间找到值

⚡️ 平均时间复杂度:O(1) ,比数组的 O(n) 快得多!

JavaScript 中的哈希表实现

数据结构是否支持任意键是否有序推荐场景
Object❌ 只支持字符串/符号❌ 无序简单映射
Map✅ 支持任意类型✅ 有序算法题首选

所以在本题中,我们选择 new Map() 是最优解。


核心思想总结:三句口诀

记住这三句话,你在任何面试中都能自信应对:

🔹 用哈希 map
🔹 空间换时间
🔹 求和变求差

这不仅是解题技巧,更是算法工程师的思维方式


学习建议:刷题 ≠ 背答案

正确的学习路径:

  1. 刷 LeetCode Hot 100,至少刷三遍
  2. 每道题都要理解其背后的数据结构与算法思想
  3. 不仅要会写代码,还要能讲清楚为什么这么写
  4. 主动对比不同解法的时间/空间复杂度

错误的做法:

  • 只背代码模板,不理解逻辑
  • 遇到难题就跳过,不去思考
  • 写完就不管,没有复盘

实战演示:一步一步走一遍

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

inums[i]complementdiffs 中是否有?操作
029-2=7存入 {2:0}
179-7=2是!(2 在 index 0)返回 [0,1] ✅

完美命中!


总结:不只是解一道题

《两数之和》看似简单,但它教会了我们:

  • 如何用哈希表解决查找问题
  • 如何进行思维转换(求和 → 求差)
  • 如何权衡时间和空间复杂度
  • 如何在面试中清晰表达思路

这些能力,远比写出一段正确的代码更重要。


最后送你一句金句:

“算法不是为了炫技,而是为了解决问题。”

当你能在 1 分钟内说出“用哈希表,空间换时间,求和变求差”这三句话时,你就已经迈过了大多数人的门槛。

现在,打开你的编辑器,把这段代码敲一遍,然后大声念出那三句话吧!