题目(LeetCode #1)
给定一个整数数组nums和一个整数目标值target,请你在该数组中找出 和为目标值 的那 两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,且同一个元素不能使用两次。
这道题看似简单,却是无数程序员刷 LeetCode 的起点,更是大厂面试官用来“筛人”的第一道关卡。它不仅考察基础编码能力,更暗藏对算法思维、空间时间权衡、数据结构理解的深度检验。
本文将带你从暴力解法到哈希优化,再到面试官真正想看到的思考过程,彻底掌握“两数之和”的多种解法与学习方法论。
一、暴力解法:O(n²) 的朴素思路
✅ 思路
遍历数组中的每一个元素 nums[i],再嵌套一层循环查找是否存在 nums[j],使得:
js
编辑
nums[i] + nums[j] === target
💻 代码实现
javascript
编辑
function twoSum(nums, target) {
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²) —— 两层循环
- 空间复杂度:O(1) —— 只用常量额外空间
❌ 为什么面试官不喜欢?
- 效率低下,无法应对大规模数据
- 暴露候选人缺乏“优化意识”
- 容易被质疑:“是不是只背了答案?”
🔍 面试官潜台词:
“如果连最基础的优化都想不到,后续更复杂的系统设计怎么办?”
二、哈希表优化:O(n) 的经典解法
✅ 核心思想:把“求和”转化为“求差”
我们不再问:“有没有另一个数能和我相加等于 target?”
而是问:“我已经见过哪些数?当前数需要的‘搭档’是否已经出现过? ”
即:
js
编辑
diff = target - currentNumber
如果 diff 已经在之前的遍历中出现过,那么答案就找到了!
🧠 关键洞察:用空间换时间
- 用一个哈希表(对象或 Map)记录 “数值 → 下标” 的映射
- 遍历一次数组,每次先查表,再存入当前值
解法 1:使用普通对象(Object)—— 兼容 ES5
javascript
编辑
function twoSum(nums, target) {
const map = {}; // 哈希表:key=数值, value=下标
for (let i = 0; i < nums.length; i++) {
const n = nums[i];
const diff = target - n;
// 检查 diff 是否已存在(注意:0 是合法下标!)
if (map[diff] !== undefined) {
return [map[diff], i];
}
// 记录当前数值及其下标
map[n] = i;
}
}
⚠️ 注意:必须用
!== undefined判断,因为下标可能是0,而if (map[diff])会误判!
解法 2:使用 ES6 Map —— 更安全、语义更清晰
javascript
编辑
function twoSum(nums, target) {
const map = new Map(); // 推荐:支持任意类型 key,无原型链干扰
for (let i = 0; i < nums.length; i++) {
const n = nums[i];
const diff = target - n;
if (map.has(diff)) {
return [map.get(diff), i];
}
map.set(n, i);
}
}
✅ 优势对比
| 特性 | Object | Map |
|---|---|---|
| 查找时间复杂度 | O(1) 平均 | O(1) 严格保证 |
| 支持 key 类型 | 仅字符串/symbol | 任意类型(包括对象) |
| 是否有原型污染风险 | 有(如 __proto__) | 无 |
| 语义清晰度 | 一般 | 高(专为键值对设计) |
💡 建议:在算法题中优先使用
Map,尤其当 key 可能为负数、0 或非字符串时。
⏱ 复杂度分析(哈希解法)
- 时间复杂度:O(n) —— 只需遍历一次
- 空间复杂度:O(n) —— 最坏情况下哈希表存 n 个元素
✅ 典型的空间换时间策略,也是面试官希望看到的“算法意识”。
三、面试官真正想考察什么?
1. 是否具备“问题转化”能力
- 能否把“找两个数之和”转化为“找一个数是否存在”
- 这是算法思维的核心:将复杂问题简化为已知模型
2. 是否理解数据结构的适用场景
- 为什么选哈希表?因为快速查找是核心需求
- 如果用数组或列表存储,查找仍是 O(n),总复杂度退化为 O(n²)
3. 边界条件处理意识
- 数组为空?
- 有重复元素?(本题允许,但下标唯一)
- 目标值为负数?(不影响逻辑)
4. 代码健壮性
- 使用
map.has()而不是map[key]避免类型陷阱 - 不修改原数组,不使用全局变量
四、延伸思考:还能怎么考?
虽然“两数之和”是入门题,但它的变体层出不穷:
| 变体 | 考察点 |
|---|---|
| 三数之和(LeetCode #15) | 排序 + 双指针,去重逻辑 |
| 两数之和 II(有序数组) | 双指针优化,无需哈希 |
| 两数之和 IV(BST 中找两数) | 树的遍历 + 哈希 or 双指针 |
| 小于目标值的两数之和对数 | 排序 + 双指针计数 |
🌱 学习建议:
不要只满足于 AC 一道题,而是思考: “这个问题的本质是什么?还能怎么变形?”
五、总结:从一道题学会高效学习
| 层级 | 行为 | 结果 |
|---|---|---|
| Level 1 | 背答案、记代码 | 面试一问就懵 |
| Level 2 | 理解暴力解法 | 能写,但效率低 |
| Level 3 | 掌握哈希优化 | 通过基础面试 |
| Level 4 | 理解“转化思想”+“数据结构选择” | 举一反三,应对变体 |
| Level 5 | 主动思考边界、复杂度、工程实践 | 成为面试官眼中的“潜力股” |
✅ 最终建议
- 动手写两种解法,对比性能差异
- 在纸上画执行流程图(如哈希表如何一步步构建)
- 尝试讲解给他人听 —— 能讲清楚,才算真懂
- 不要止步于“通过” ,多问一句:“还有更好的方法吗?”
💬 一句话总结:
“两数之和”不是一道题,而是一扇门——
门后是算法思维、数据结构、工程意识的广阔世界。
愿你从此题出发,走向更强大的自己。🚀