两数之和:从暴力到哈希,揭秘大厂面试的「算法门面」题

96 阅读7分钟

作为算法入门的「Hello World」,两数之和几乎是所有程序员的算法启蒙题。但你可能不知道,这道题也是大厂面试的「刷人利器」—— 面试官不仅考察你会不会做,更要看你能不能说清思路演进、理解底层逻辑,甚至从代码里判断你是「死记硬背热点题」还是「真正懂算法」。

今天我们就从「暴力解法」到「哈希优化」,一步步拆解这道题的核心逻辑,不仅教会你怎么写,更让你明白「为什么这么写」,帮你在面试中脱颖而出。

一、题目回顾:两数之和到底要做什么?

先明确题目要求(LeetCode 第一题标准描述):

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

示例:输入:nums = [2, 7, 11, 15], target = 9输出:[0, 1]解释:因为 nums[0] + nums[1] = 2 + 7 = 9,所以返回下标 0 和 1。

二、初遇:暴力解法,简单但低效

拿到题目第一反应是什么?遍历所有可能的组合,找到和为 target 的两个数。这就是「暴力解法」的核心思路。

暴力解法代码实现

javascript

运行

function twoSum(nums, target) {
  const len = nums.length;
  // 第一层循环:遍历每个元素作为第一个数
  for (let i = 0; i < len; i++) {
    // 第二层循环:遍历当前元素后面的所有元素作为第二个数
    for (let j = i + 1; j < len; j++) {
      if (nums[i] + nums[j] === target) {
        return [i, j];
      }
    }
  }
  // 题目保证有答案,这里可省略
  return [];
}

暴力解法的优缺点

  • 优点:逻辑简单,上手快,不需要额外的数据结构知识,新手也能快速写出。
  • 缺点:时间复杂度是 O(n²)—— 当数组长度 n 很大时(比如 n=10000),需要执行 100 万次循环,效率极低,在大厂面试中几乎会被直接否定。

面试官怎么看暴力解法?

如果你只写出暴力解法,面试官大概率会追问:「有没有更优的解法?」。这道题的暴力解法就像「入门门槛」,只能证明你「会做题」,但证明不了你「懂算法」—— 算法的核心是「用空间换时间」或「用时间换空间」的权衡,而暴力解法既没利用空间,也没优化时间。

三、进阶:哈希表优化,用空间换时间

要优化时间复杂度,核心思路是「减少查找次数」。暴力解法中,第二层循环的目的是「找 target - nums [i] 是否存在于数组中」,而数组的查找时间是 O(n)。如果能把查找时间降到 O(1),整体时间复杂度就能降到 O(n)

这时候,「哈希表」(HashMap)就该登场了 —— 哈希表的查找、插入操作都是 O(1) 时间复杂度,完美解决了「快速查找」的问题。

核心思路:把「求和」变成「求差」

  1. 遍历数组时,对于当前元素 nums[i],计算出「需要找到的互补数」:complement = target - nums[i]

  2. 检查哈希表中是否已经存在这个「互补数」:

    • 如果存在,直接返回互补数的下标和当前元素的下标。
    • 如果不存在,把当前元素 nums[i] 和它的下标 i 存入哈希表,供后续元素查找。

简单说:我们不再用两层循环找「和为 target 的两个数」,而是用一层循环找「当前数的互补数是否已出现」—— 这就是「用空间换时间」的精髓。

哈希表实现(ES6 Map 版本)

javascript

运行

function twoSum(nums, target) {
  const diffs = new Map(); // 哈希表:key=数组元素,value=元素下标
  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);
  }
  
  return [];
}

哈希表实现(ES5 对象版本)

如果面试中要求兼容 ES5,也可以用普通对象模拟哈希表(对象的键值对本质就是一种哈希结构):

javascript

运行

function twoSum(nums, target) {
  const diffs = {}; // 键=数组元素,值=元素下标
  const len = nums.length;
  
  for (let i = 0; i < len; i++) {
    const complement = target - nums[i];
    // 检查对象中是否有互补数(注意避免键为 0 或空字符串的误判)
    if (diffs.hasOwnProperty(complement)) {
      return [diffs[complement], i];
    }
    diffs[nums[i]] = i;
  }
  
  return [];
}

哈希解法的优缺点

  • 优点:时间复杂度 O(n),只遍历一次数组;空间复杂度 O(n),最多存储 n-1 个元素(因为题目保证有答案,找到时循环终止),效率极高。
  • 缺点:需要额外占用哈希表的空间,对于空间极度敏感的场景(比如嵌入式开发)可能需要权衡,但在大多数业务场景和面试中,这是最优解。

面试官会追问的细节

写出哈希解法后,面试官大概率会追问这些问题,考验你的细节把控:

  1. 为什么用 Map 而不是普通对象?

    • 普通对象的键只能是字符串或 Symbol,当数组元素是 0undefined 等时,可能出现误判(比如 diffs[0] 会被当作 false);
    • Map 的键可以是任意类型(包括数字、对象等),且有 has()get() 等方法,语义更清晰,不易出错。
  2. 为什么要先检查互补数,再存入当前元素?

    • 避免「使用同一个元素两次」的问题。比如 nums = [3, 3], target = 6,如果先存入 3,再检查互补数 3,会直接返回 [0, 1];如果先检查再存入,就不会出现自己和自己相加的情况。
  3. 哈希表的查找时间真的是 O (1) 吗?

    • 理想情况下是 O (1),但如果出现哈希冲突(不同键映射到同一个哈希值),查找时间会退化到 O (n);
    • 但主流语言的哈希表(比如 JavaScript 的 Map)都有冲突解决机制(比如链地址法),实际使用中平均查找时间接近 O (1)。

四、这道题背后的大厂面试逻辑

为什么大厂总爱考两数之和?因为它能快速筛选出「真懂算法」和「假懂算法」的人:

  1. 考察基础数据结构:是否理解哈希表的核心作用(快速查找);
  2. 考察算法思想:是否掌握「用空间换时间」的核心权衡;
  3. 考察代码细节:是否注意到边界条件(比如重复元素、特殊值);
  4. 考察表达能力:能否清晰解释从暴力解法到哈希解法的演进思路。

如果面试时,你能从「暴力解法的痛点」出发,一步步推导到「哈希解法的优化逻辑」,再解释清楚细节问题,面试官会认为你不仅会做题,更懂算法的本质 —— 这正是大厂想要的人才。

五、总结

两数之和看似简单,但它是算法学习的「敲门砖」:

  • 暴力解法:入门级思路,逻辑简单但效率低(O (n²));
  • 哈希解法:进阶级思路,用空间换时间(O (n) 时间 + O (n) 空间),是面试最优解。

这道题的核心启示是:算法的本质是「权衡」—— 没有绝对最优的解法,只有适合场景的解法。在实际开发中,我们往往会选择「时间换空间」或「空间换时间」,而哈希表正是「空间换时间」的典型应用。

希望这篇文章能帮你不仅「会做」两数之和,更能「懂透」它背后的逻辑。下次面试再遇到这道题,不妨自信地从暴力解法说起,一步步推导到哈希解法,让面试官看到你的思考过程 —— 这比直接写出答案更重要。