两数之和:从 JSON 对象到 Map,大厂面试官到底在考察什么?
导读:LeetCode Hot 100 第一题“两数之和”,看似简单,却是大厂面试的“照妖镜”。为什么有人用对象写挂了?为什么面试官更偏爱
Map?本文结合 MDN 文档与实战代码,带你从底层原理到进阶技巧,彻底吃透这道算法门面题。
🎯 面试官的潜台词:你是在“背题”还是“懂原理”?
在字节、腾讯、阿里的技术面试中,“两数之和”往往是开场第一题。
- 如果你秒出暴力解法:面试官心想“基础一般,继续观察”。
- 如果你直接甩出 Hash Map 解法但说不清原理:面试官怀疑“是不是刚刷完题库?”。
- 如果你能对比
Object与Map的优劣,并说出底层差异:面试官点头“这个人有深度,通过”。
今天,我们就从最原始的 JSON 对象模拟哈希表开始,一步步进阶到 ES6 Map,并结合 MDN 文档深入剖析。
🚀 第一阶段:利用 JSON 对象模拟 Hash Map(ES5 经典解法)
在 ES6 之前,JavaScript 没有原生的哈希表结构。开发者通常使用普通对象 {} 来模拟。
核心思路:空间换时间
将 的双重循环优化为 的单次循环。
- 暴力法:遍历每个元素
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 数据结构,存在以下隐患:
- 键类型限制:对象的键自动转换为字符串。如果
nums中有数字1和字符串"1",在对象中会冲突。 - 原型链污染:如果不小心使用了
toString或constructor作为键,可能会读取到原型链上的方法。 - 遍历顺序:早期 JS 引擎对对象属性遍历顺序不保证(虽然后期规范了数字键的顺序,但仍不如 Map 严谨)。
🚀 第二阶段:ES6 Map 进阶(大厂推荐解法)
ES6 引入了 Map 数据结构,它是真正的 Hash Map 实现。
为什么面试官更喜欢 Map?
- 键的类型任意:可以是对象、函数、NaN,甚至
-0和+0都能区分。 - 纯净性:没有原型链干扰,
size属性直接获取大小,无需手动计数。 - 性能优化:在频繁增删场景下,
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 Docs,Map 的核心 API 如下,这也是面试中可能考察的细节:
| 方法 | 描述 | 对应 Object 操作 | 优势 |
|---|---|---|---|
new Map() | 创建一个新 Map | {} | 纯净,无原型污染 |
set(key, value) | 添加或更新键值对 | obj[key] = value | 返回 Map 本身,支持链式调用 |
get(key) | 返回键对应的值 | obj[key] | 能处理任意类型的键 |
has(key) | 判断键是否存在 | key in obj 或 obj.hasOwnProperty(key) | 最快且最安全,无 falsy 误判 |
delete(key) | 删除指定键值对 | delete obj[key] | 返回布尔值表示是否删除成功 |
clear() | 清空所有元素 | 需手动遍历删除 | 一键清空,高效 |
size | 返回元素个数 | 需手动计数或 Object.keys().length | O(1) 时间复杂度获取 |
为什么 has() 比 !== undefined 更好?
在 Object 方案中,我们必须写 if (diff[complement] !== undefined) 来避免索引 0 被误判。
而在 Map 中,if (diff.has(complement)) 语义明确:只关心键是否存在,完全不关心值是什么。这大大降低了代码出错的可能性。
🧠 大厂面试官的“灵魂拷问”
当你写出上述代码后,面试官可能会追问:
Q1: 如果数组中有重复元素怎么办?
A: 题目保证“每种输入只会对应一个答案”,且“不能重复利用同一个元素”。我们的逻辑是先查后存。 例如
nums=[3, 3], target=6。
- i=0, num=3, complement=3。Map 为空,存入
{3: 0}。- i=1, num=3, complement=3。Map 中存在 3,返回
[0, 1]。 即使后面有相同的值覆盖了前面的索引,也不影响已经找到的结果。
Q2: 时间复杂度和空间复杂度是多少?
A:
- 时间复杂度:。我们只遍历了一次数组,每次
Map的查找和插入操作平均为 。- 空间复杂度:。最坏情况下,我们需要将 个元素存入 Map。
Q3: 为什么不用暴力解法?
A: 暴力解法时间复杂度 。当数据量达到 级别时,运算次数高达 ,会导致超时(Time Limit Exceeded)。而 解法可以轻松处理 级别的数据。这是工程化思维的体现。
📝 总结
“两数之和”不仅仅是一道算法题,它是 JavaScript 数据结构演进的缩影:
- ES5 时代:我们用
Object勉力模拟,需小心undefined和原型链陷阱。 - ES6 时代:
Map应运而生,提供了语义化、高性能、类型安全的哈希表实现。
给求职者的建议:
- 不要只背代码,要理解 “求和变求差” 的数学转换思想。
- 不要只写
Object,在涉及键值对映射的算法题中,优先使用Map,这体现了你对现代 JS 特性的掌握。 - 时刻警惕 边界条件(如索引 0、重复值、空数组),这是区分“码农”和“工程师”的关键。
参考链接:
觉得有用吗?欢迎点赞、收藏、关注,下期带你拆解 Hot 100 第二题! 👇