两数之和——算法题中的“abandon”

49 阅读5分钟

前言:一道“门面”题的自我修养

在程序员的面试江湖中,“两数之和”的地位不可撼动。它既是 算法入门的敲门砖,也是 大厂面试的照妖镜

很多同学觉得这道题简单,刷一遍就过了。但正如你的笔记中所言,面试官考这道题的心态往往是:

  1. 基础筛选:这是算法界的“1+1”,如果连这道题都做不出来,或者只能写出暴力解法,基本就“走远了”。
  2. 试探态度:这是一道绝对的热点题。如果你没做过或没准备好最优解,说明你对面试缺乏敬畏之心。
  3. 考察思维:看你是否具备 “空间换时间” 的优化意识,以及对语言内置数据结构(HashMap)的掌握程度。

今天,我们就把这个算法题里的“abandon”彻底消化掉。


题目回顾

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例:

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

⛔️ 阶段一:暴力破解 (Brute Force)

这是最直观的思路,也是很多初学者的第一反应。

思路

直接写两个循环。第一层循环拿一个数 x,第二层循环遍历剩下的数,看有没有一个数 y 使得 x + y === target。

复杂度分析 (重点)

  • 时间复杂度:O(n^2)

    • 由于使用了双重 for 循环,在最坏的情况下,需要遍历 2n(n−1)​/2次。随着数据量 n 的增大,耗时呈指数级增长,效率非常低。
  • 空间复杂度:O(1)

    • 只需要常数个变量来存储临时数据,不需要额外的存储空间。

结论:在面试中写出这个解法,通常意味着“回家等通知”。我们需要更高效的方法。


🚀 阶段二:哈希表 (Hash Map) —— 空间换时间

如何将 O(n^2) 降为 O(n)?核心在于消除内层循环

核心思维:加法变减法

与其苦苦寻找两个匹配的数在哪里,不如在遍历数组的过程中,记录下我们依然需要的数(或者记录下我们需要寻找的另一半)。

我们将问题转化为:

遍历数组,对于每一个 nums[i],我先去“备忘录”里查一下:有没有谁之前在找我? (即 target - nums[i] 是否存在于备忘录中)。

  • 如果:那我们就配对成功了,直接返回结果。
  • 如果没有:那我就把我自己(nums[i])和我的下标存入备忘录,等待我的“另一半”来找我。

这个“备忘录”,就是 哈希表 (Hash Map)


实现方案 A:ES5 时代的 Object

在 ES6 普及之前,JavaScript 开发者通常使用对象(Object)来模拟哈希表。利用对象属性查找速度快的特性。

/* ES5 写法:使用 Object 模拟 HashMap */
function twoSum(nums, target) {
  // diffs 就是我们的“备忘录”
  const diffs = {}; 
  const len = nums.length;

  for (let i = 0; i < len; i++) {
    // 计算为了凑齐 target,当前数字需要的“另一半”是多少
    const complement = target - nums[i];
    
    // 查表:看看这一半是否已经在前面出现过
    if (diffs[complement] !== undefined) {
      // 找到了!返回 [之前的那个数的下标, 当前下标]
      return [diffs[complement], i];
    }
    
    // 没找到,把自己登记到备忘录里
    // key: 数组中的数值, value: 数组的下标
    diffs[nums[i]] = i;
  }
}

缺点

  1. Object 的 key 主要是字符串(虽然 JS 会自动转换数字,但存在类型隐患)。
  2. Object 原型链上可能存在默认属性,不够纯粹。

实现方案 B:ES6 时代的 Map (推荐) ✨

ES6 提供了标准的 Map 数据结构,它是更纯粹的哈希表,专为键值对存储设计。

/* ES6 写法:使用内置 Map 数据结构 */
function twoSum(nums, target) {
  // 创建一个纯净的哈希表
  const diffs = new Map();
  const len = nums.length;

  for (let i = 0; i < len; i++) {
    const complement = target - nums[i];

    // Map 的 has 方法查询效率很高
    if (diffs.has(complement)) {
      // get 方法获取对应的值(下标)
      return [diffs.get(complement), i];
    }

    // 将当前数值和下标存入 Map
    // key: nums[i], value: i
    diffs.set(nums[i], i);
  }
}

复杂度分析 (优化后)

  • 时间复杂度:O(n)

    • 我们只遍历了包含 n 个元素的数组一次。
    • 在哈希表中进行查找(has)和插入(set)操作,平均时间复杂度是 O(1)。
    • 所以总时间为 n * 1 = O(n)。
  • 空间复杂度:O(n)

    • 我们需要一个 Map 来存储元素。在最坏的情况下(比如找不到匹配,或者匹配的数在数组最后两个),我们需要存储 n 个元素。

💡 深度思考:Object vs Map

在面试官问完代码后,可能会顺口问一句: “这里为什么用 Map 而不用 Object?”  或者是  “这两者有什么区别?”

你可以这样回答,以此展示基础扎实:

  1. 键的类型:Object 的键传统上只能是 String 或 Symbol(数字会自动转字符串),而 Map 的键可以是任意类型(包括函数、对象、基本类型)。虽然在本题中是存数字,但 Map 语义更清晰。
  2. 性能:在频繁增删键值对的场景下,Map 针对性能进行了优化,通常表现优于 Object。
  3. 纯净度:Object 拥有原型链,可能会意外访问到原型上的属性(例如 toString),而 Map 是“干净”的。
  4. 大小获取:Map 可以直接通过 .size 获取元素个数,Object 需要 Object.keys().length。

总结

“两数之和”虽是第一题,但它包含了算法优化的核心思想——时空权衡 (Time-Space Tradeoff)

  • 暴力解法:省空间,费时间。(O(1) Space, O(n^2) Time)
  • 哈希解法:费空间,省时间。(O(n) Space, O(n) Time)

当你下次在面试中遇到它,请不要只满足于写出答案,试着向面试官展示你对 Hash Map 原理的理解以及 ES6 Map vs Object 的细节把控。这才是从“做题”到“工程思维”的跨越。

希望这篇文章能帮你把算法路上的这块“绊脚石”,变成垫脚石。Abandon? No, Accepted!