作为算法入门的「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) 时间复杂度,完美解决了「快速查找」的问题。
核心思路:把「求和」变成「求差」
-
遍历数组时,对于当前元素
nums[i],计算出「需要找到的互补数」:complement = target - nums[i]。 -
检查哈希表中是否已经存在这个「互补数」:
- 如果存在,直接返回互补数的下标和当前元素的下标。
- 如果不存在,把当前元素
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 个元素(因为题目保证有答案,找到时循环终止),效率极高。 - 缺点:需要额外占用哈希表的空间,对于空间极度敏感的场景(比如嵌入式开发)可能需要权衡,但在大多数业务场景和面试中,这是最优解。
面试官会追问的细节
写出哈希解法后,面试官大概率会追问这些问题,考验你的细节把控:
-
为什么用 Map 而不是普通对象?
- 普通对象的键只能是字符串或 Symbol,当数组元素是
0、undefined等时,可能出现误判(比如diffs[0]会被当作false); - Map 的键可以是任意类型(包括数字、对象等),且有
has()、get()等方法,语义更清晰,不易出错。
- 普通对象的键只能是字符串或 Symbol,当数组元素是
-
为什么要先检查互补数,再存入当前元素?
- 避免「使用同一个元素两次」的问题。比如
nums = [3, 3], target = 6,如果先存入 3,再检查互补数 3,会直接返回 [0, 1];如果先检查再存入,就不会出现自己和自己相加的情况。
- 避免「使用同一个元素两次」的问题。比如
-
哈希表的查找时间真的是 O (1) 吗?
- 理想情况下是 O (1),但如果出现哈希冲突(不同键映射到同一个哈希值),查找时间会退化到 O (n);
- 但主流语言的哈希表(比如 JavaScript 的 Map)都有冲突解决机制(比如链地址法),实际使用中平均查找时间接近 O (1)。
四、这道题背后的大厂面试逻辑
为什么大厂总爱考两数之和?因为它能快速筛选出「真懂算法」和「假懂算法」的人:
- 考察基础数据结构:是否理解哈希表的核心作用(快速查找);
- 考察算法思想:是否掌握「用空间换时间」的核心权衡;
- 考察代码细节:是否注意到边界条件(比如重复元素、特殊值);
- 考察表达能力:能否清晰解释从暴力解法到哈希解法的演进思路。
如果面试时,你能从「暴力解法的痛点」出发,一步步推导到「哈希解法的优化逻辑」,再解释清楚细节问题,面试官会认为你不仅会做题,更懂算法的本质 —— 这正是大厂想要的人才。
五、总结
两数之和看似简单,但它是算法学习的「敲门砖」:
- 暴力解法:入门级思路,逻辑简单但效率低(O (n²));
- 哈希解法:进阶级思路,用空间换时间(O (n) 时间 + O (n) 空间),是面试最优解。
这道题的核心启示是:算法的本质是「权衡」—— 没有绝对最优的解法,只有适合场景的解法。在实际开发中,我们往往会选择「时间换空间」或「空间换时间」,而哈希表正是「空间换时间」的典型应用。
希望这篇文章能帮你不仅「会做」两数之和,更能「懂透」它背后的逻辑。下次面试再遇到这道题,不妨自信地从暴力解法说起,一步步推导到哈希解法,让面试官看到你的思考过程 —— 这比直接写出答案更重要。