两数之和:从 JSON 对象到 Map,大厂面试官到底在考察什么?

4 阅读6分钟

两数之和:从 JSON 对象到 Map,大厂面试官到底在考察什么?

导读:LeetCode Hot 100 第一题“两数之和”,看似简单,却是大厂面试的“照妖镜”。为什么有人用对象写挂了?为什么面试官更偏爱 Map?本文结合 MDN 文档与实战代码,带你从底层原理到进阶技巧,彻底吃透这道算法门面题。


🎯 面试官的潜台词:你是在“背题”还是“懂原理”?

在字节、腾讯、阿里的技术面试中,“两数之和”往往是开场第一题。

  • 如果你秒出暴力解法:面试官心想“基础一般,继续观察”。
  • 如果你直接甩出 Hash Map 解法但说不清原理:面试官怀疑“是不是刚刷完题库?”。
  • 如果你能对比 ObjectMap 的优劣,并说出底层差异:面试官点头“这个人有深度,通过”。

今天,我们就从最原始的 JSON 对象模拟哈希表开始,一步步进阶到 ES6 Map,并结合 MDN 文档深入剖析。


🚀 第一阶段:利用 JSON 对象模拟 Hash Map(ES5 经典解法)

在 ES6 之前,JavaScript 没有原生的哈希表结构。开发者通常使用普通对象 {} 来模拟。

核心思路:空间换时间

O(n2)O(n^2) 的双重循环优化为 O(n)O(n) 的单次循环。

  • 暴力法:遍历每个元素 x,再遍历后续元素寻找 target - x
  • 哈希法:遍历每个元素 x,计算 complement = target - x如果 complement 已经在哈希表中,直接返回;否则将 x 存入哈希表。

代码实现与陷阱修复

很多初学者的代码会死在 索引 0 上。请看下面的错误示范与修正:

// ❌ 常见错误写法
if (diff[complement]) { 
    // 当 diff[complement] 为 0 时(即第一个元素匹配),0 是 falsy,条件不成立!
    return [diff[complement], i];
}

✅ 正确写法(生产级):

function twoSum(nums, target) {
    const diff = {}; // 使用普通对象模拟 Hash Map
    const len = nums.length;
    
    for (let i = 0; i < len; i++) {
        const complement = target - nums[i];
        
        // 【关键点】必须显式判断 !== undefined
        // 因为数组索引可能是 0,而 if(0) 为 false
        if (diff[complement] !== undefined) {
            return [diff[complement], i];
        }
        
        // 处理 value 相同的情况:后出现的会覆盖先出现的索引
        // 但在两数之和中,只要找到一对即可,覆盖不影响结果
        diff[nums[i]] = i;
    }
    
    return null;
}

💡 对象作为 Hash 表的局限性

虽然对象好用,但它本质是 JSON 数据结构,存在以下隐患:

  1. 键类型限制:对象的键自动转换为字符串。如果 nums 中有数字 1 和字符串 "1",在对象中会冲突。
  2. 原型链污染:如果不小心使用了 toStringconstructor 作为键,可能会读取到原型链上的方法。
  3. 遍历顺序:早期 JS 引擎对对象属性遍历顺序不保证(虽然后期规范了数字键的顺序,但仍不如 Map 严谨)。

🚀 第二阶段:ES6 Map 进阶(大厂推荐解法)

ES6 引入了 Map 数据结构,它是真正的 Hash Map 实现。

为什么面试官更喜欢 Map

  1. 键的类型任意:可以是对象、函数、NaN,甚至 -0+0 都能区分。
  2. 纯净性:没有原型链干扰,size 属性直接获取大小,无需手动计数。
  3. 性能优化:在频繁增删场景下,Map 的性能通常优于普通对象。

对比演示:Object vs Map

让我们通过一段代码直观感受两者的区别:

// --- 普通对象 Object ---
const obj = {
    name: 'susu',
    company: '字节跳动'
};

// 动态添加/修改属性
obj.age = 18; 
obj.age = 23; // 直接覆盖
console.log(obj.age); // 23
console.log(obj['name']); // 'susu'
// 缺点:键只能是字符串或 Symbol,无法区分 1 和 "1"

// --- ES6 Map ---
const o = new Map();

// 设置键值对 (key, value)
o.set('name', 'baga');
o.set('company', '腾讯');
o.set(1, '数字键1'); 
o.set('1', '字符串键1'); // Map 可以区分这两个键!

// 获取值
console.log(o.get('company')); // '腾讯'
console.log(o.get(1)); // '数字键1'
console.log(o.get('1')); // '字符串键1'
console.log(o.size); // 4 (自动维护大小)

// 检查是否存在
console.log(o.has('name')); // true

// 删除
o.delete('name');

✅ 两数之和的 Map 终极解法

function twoSum(nums, target) {
    // 初始化 Map
    const diff = new Map();
    const len = nums.length;
    
    for (let i = 0; i < len; i++) {
        const complement = target - nums[i];
        
        // Map 提供专门的 has() 方法,语义更清晰,且无 falsy 值陷阱
        if (diff.has(complement)) {
            return [diff.get(complement), i];
        }
        
        // set(key, value) 存储数值和索引
        diff.set(nums[i], i);
    }
    
    return null;
}

📚 深度解析:MDN 中的 Map API 核心方法

根据 MDN Web DocsMap 的核心 API 如下,这也是面试中可能考察的细节:

方法描述对应 Object 操作优势
new Map()创建一个新 Map{}纯净,无原型污染
set(key, value)添加或更新键值对obj[key] = value返回 Map 本身,支持链式调用
get(key)返回键对应的值obj[key]能处理任意类型的键
has(key)判断键是否存在key in objobj.hasOwnProperty(key)最快且最安全,无 falsy 误判
delete(key)删除指定键值对delete obj[key]返回布尔值表示是否删除成功
clear()清空所有元素需手动遍历删除一键清空,高效
size返回元素个数需手动计数或 Object.keys().lengthO(1) 时间复杂度获取

为什么 has()!== undefined 更好?

Object 方案中,我们必须写 if (diff[complement] !== undefined) 来避免索引 0 被误判。 而在 Map 中,if (diff.has(complement)) 语义明确:只关心键是否存在,完全不关心值是什么。这大大降低了代码出错的可能性。


🧠 大厂面试官的“灵魂拷问”

当你写出上述代码后,面试官可能会追问:

Q1: 如果数组中有重复元素怎么办?

A: 题目保证“每种输入只会对应一个答案”,且“不能重复利用同一个元素”。我们的逻辑是先查后存。 例如 nums=[3, 3], target=6

  1. i=0, num=3, complement=3。Map 为空,存入 {3: 0}
  2. i=1, num=3, complement=3。Map 中存在 3,返回 [0, 1]。 即使后面有相同的值覆盖了前面的索引,也不影响已经找到的结果。

Q2: 时间复杂度和空间复杂度是多少?

A:

  • 时间复杂度O(n)O(n)。我们只遍历了一次数组,每次 Map 的查找和插入操作平均为 O(1)O(1)
  • 空间复杂度O(n)O(n)。最坏情况下,我们需要将 n1n-1 个元素存入 Map。

Q3: 为什么不用暴力解法?

A: 暴力解法时间复杂度 O(n2)O(n^2)。当数据量达到 10410^4 级别时,运算次数高达 10810^8,会导致超时(Time Limit Exceeded)。而 O(n)O(n) 解法可以轻松处理 10610^6 级别的数据。这是工程化思维的体现。


📝 总结

“两数之和”不仅仅是一道算法题,它是 JavaScript 数据结构演进的缩影:

  1. ES5 时代:我们用 Object 勉力模拟,需小心 undefined 和原型链陷阱。
  2. ES6 时代Map 应运而生,提供了语义化、高性能、类型安全的哈希表实现。

给求职者的建议

  • 不要只背代码,要理解 “求和变求差” 的数学转换思想。
  • 不要只写 Object,在涉及键值对映射的算法题中,优先使用 Map,这体现了你对现代 JS 特性的掌握。
  • 时刻警惕 边界条件(如索引 0、重复值、空数组),这是区分“码农”和“工程师”的关键。

参考链接

觉得有用吗?欢迎点赞、收藏、关注,下期带你拆解 Hot 100 第二题! 👇