作为 LeetCode 第一题,两数之和是算法入门的必练经典题,也是前端面试中高频考察的基础题。这道题看似简单,却能很好地考察对数组操作、时间 / 空间复杂度优化和哈希表核心思想的理解,同时能区分新手和有工程思维的开发者 —— 从暴力枚举到哈希表优化,正是「基础实现」到「最优解」的典型思路。
本文将用 JavaScript 实现两种解法,从思路分析、代码实现、复杂度拆解、细节深挖四个维度逐一讲解,同时补充面试高频考点、新手避坑指南、拓展场景,帮你彻底吃透这道题的核心逻辑,做到举一反三。
一、题目要求
给定一个整数数组 nums 和一个整数目标值 target,请在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。
约束条件
每种输入只会对应一个有效答案;
不能使用两次相同的元素;
可以按任意顺序返回答案。
测试示例
二、解法一:暴力枚举法(新手入门必学)
暴力枚举是最直观的解法,核心是通过双层循环模拟「逐个匹配」的过程,适合算法新手理解题目的核心要求,也是后续优化的基础。
- 核心思路
遍历数组中的每一个元素 nums[i],将其作为第一个数,然后从该元素的下一个位置开始遍历剩余元素 nums[j](j > i),将其作为第二个数,判断两者之和是否等于 target。由于题目保证有且仅有一个有效答案,因此一旦找到符合条件的 i 和 j,可直接返回结果,无需继续遍历。
/** * @param {number[]} nums 整数数组 * @param {number} target 目标和 * @return {number[]} 两个数的数组下标 */functiontwoSum(nums,target){// 外层循环:确定第一个数的下标和值for(leti=0;i<nums.length;i++){// 内层循环:从i+1开始,避免重复使用元素和重复判断for(letj=i+1;j<nums.length;j++){// 找到和为target的两个数,直接返回下标if(nums[i]+nums[j]===target){return[i,j];}}}return[];// 题目保证有解,理论上不会执行到此处}// 测试示例console.log(twoSum([2,7,11,15],9));// [0,1]console.log(twoSum([3,2,4],6));// [1,2]console.log(twoSum([3,3],6));// [0,1]3. 关键细节深挖
很多新手会疑惑「为什么内层循环要从 i+1 开始,而不是从 0 开始?」,这是暴力法的核心细节,也是避免踩坑的关键:
避免重复使用同一个元素:如果 j 从 0 开始,会出现 i=j 的情况,此时相当于把同一个元素用了两次,违反题目要求;
减少无效循环,提升效率:如果 j 从 0 开始,会重复判断「i=0,j=1」和「i=1,j=0」这两种相同的组合,双层循环的次数会从 n(n-1)/2 变成 n²,增加不必要的计算。
4. 时间 & 空间复杂度分析
复杂度分析是算法题的必考内容,也是区分解法优劣的核心指标,需从「时间」和「空间」两个维度分析:
时间复杂度:O(n²)(n 为数组长度)
外层循环遍历数组,时间为 O(n);对于每个外层循环的元素,内层循环都要遍历剩余的 n-i-1 个元素,最坏情况下要遍历 n-1 个元素,时间也为 O(n)。双层循环的时间复杂度为两者相乘,即 O(n²)。
空间复杂度:O(1)(常数级)
仅使用了 i、j 两个循环变量,没有开辟额外的存储空间,空间复杂度为常数级。
5. 解法优缺点
✅ 优点:逻辑直观、易理解,无额外空间开销,适合算法新手入门;
❌ 缺点:时间复杂度较高,当数组长度接近题目上限 10⁴ 时,双层循环会产生约 10⁸ 次计算,执行效率大幅降低,无法满足工程上的性能要求。
三、解法二:哈希表法(最优解,面试推荐)
暴力法的核心问题是内层循环的查找效率太低(O(n)),而哈希表的查找和插入操作均为 O(1) 时间复杂度,因此我们可以用「空间换时间」的思想,通过哈希表存储已遍历的元素,将整体时间复杂度优化到 O(n),这也是这道题的最优解,面试中优先使用此解法。
1. 核心思想:补数思想
两数之和的问题可以转化为补数查找问题:对于当前遍历的元素 nums[i],要找到另一个数使得两者和为 target,这个数就是 complement = target - nums[i](补数)。因此,我们只需在已遍历过的元素中查找是否存在这个补数:
如果存在,说明补数和当前元素的和为 target,直接返回补数的下标和当前下标 i;
如果不存在,将当前元素的值和下标存入哈希表,继续遍历下一个元素。
核心优化点:用哈希表存储「元素值:元素下标」,将补数的查找效率从暴力法的 O(n) 降为 O(1)。
2. 实现方式一:普通对象实现(简洁易用)
JavaScript 中的普通对象可以天然作为简易哈希表,键(key) 存储数组元素值,值(value) 存储对应下标,适合日常开发和快速实现。
完整代码
/** * @param {number[]} nums 整数数组 * @param {number} target 目标和 * @return {number[]} 两个数的数组下标 */functiontwoSum(nums,target){constmap={};// 哈希表:key = 数组元素值,value = 元素下标// 一次遍历数组,时间复杂度O(n)for(leti=0;i<nums.length;i++){constcomplement=target-nums[i];// 计算当前元素的补数// 判断补数是否存在于哈希表中(自身属性)if(map.hasOwnProperty(complement)){return[map[complement],i];// 存在则返回补数下标和当前下标}// 不存在则将当前元素和下标存入哈希表map[nums[i]]=i;}return[];// 题目保证有解,理论上不会执行到此处}// 测试示例console.log(twoSum([2,7,11,15],9));// [0,1]console.log(twoSum([3,2,4],6));// [1,2]console.log(twoSum([3,3],6));// [0,1]关键细节深挖:为什么用 hasOwnProperty 而不是直接判断 map[complement]?
这是新手最容易踩的坑,举两个典型场景说明:
当补数的值为 0 时:如果 map[0] = 0,直接判断 if (map[0]) 会得到 false,因为 0 在 JavaScript 中是假值,会误判补数不存在;
避免原型链属性干扰:JavaScript 对象会继承 Object.prototype 的属性(如 toString、hasOwnProperty),如果数组中存在这些属性名的数值(极端场景),会导致判断错误。
hasOwnProperty 方法会仅判断对象的自身属性,忽略原型链属性,能避免上述两种坑,保证判断的准确性。
3. 实现方式二:ES6 Map 实现(更严谨,面试优先)
ES6 新增的 Map 是专门的哈希表数据结构,相比普通对象,在键值类型、判断逻辑、遍历方式上更具优势,面试中推荐使用,能体现开发者的 ES6 基础和工程严谨性。
Map 对比普通对象的核心优势
键的类型无限制:普通对象的键只能是字符串 / 符号(Symbol),而 Map 的键可以是任意类型(数字、布尔值、对象等);
判断键存在更直观:Map 提供 has() 方法专门判断键是否存在,无需像对象一样用 hasOwnProperty 规避坑;
无原型链干扰:Map 不会继承任何原型属性,天然避免属性名冲突;
可直接获取键值对数量:Map 提供 size 属性,而对象需要手动遍历才能统计键值对数量。
/** * @param {number[]} nums 整数数组 * @param {number} target 目标和 * @return {number[]} 两个数的数组下标 */functiontwoSum(nums,target){constmap=newMap();// 初始化ES6 Map哈希表for(leti=0;i<nums.length;i++){constcomplement=target-nums[i];// 计算补数if(map.has(complement)){// 判断补数是否存在return[map.get(complement),i];// 存在则获取补数下标并返回}map.set(nums[i],i);// 不存在则存入当前元素和下标}return[];// 题目保证有解,理论上不会执行到此处}// 测试示例console.log(twoSum([2,7,11,15],9));// [0,1]console.log(twoSum([3,2,4],6));// [1,2]console.log(twoSum([3,3],6));// [0,1]4. 天然避坑:避免重复使用同一个元素
哈希表法的遍历逻辑是先判断补数,再存入当前元素,因此哈希表中存储的始终是已遍历过的元素,补数不可能是当前元素本身,天然满足「不能使用两次相同元素」的题目要求,无需额外判断。
5. 时间 & 空间复杂度分析
时间复杂度:O(n)(n 为数组长度)
仅需一次遍历数组,每次遍历中的补数查找和哈希表插入操作均为 O(1) 时间复杂度,因此整体时间复杂度为 O(n),相比暴力法效率提升显著,完全适配题目中 nums.length <= 10⁴ 的约束。
空间复杂度:O(n)(n 为数组长度)
需要开辟哈希表存储已遍历的元素,最坏情况下(有效答案在数组最后两个位置),需要存储 n-1 个元素,因此空间复杂度为 O(n)。
四、面试高频考点 & 新手避坑指南
这道题虽然简单,但面试中面试官会通过追问细节考察你的基础功底和工程思维,以下是高频考点和避坑要点,务必掌握:
1. 高频追问
Q1:为什么暴力法的内层循环要从 i+1 开始?
A1:一是避免重复使用同一个元素,二是减少无效循环,避免重复判断相同的元素组合,提升效率。
Q2:为什么哈希表法能避免重复使用同一个元素?
A2:因为哈希表法是先判断补数,再存入当前元素,哈希表中只有已遍历过的元素,补数不可能是当前元素本身。
Q3:普通对象和 Map 作为哈希表,你更推荐哪种?为什么?
A3:推荐 Map,因为 Map 键的类型无限制、判断键存在更直观、无原型链属性干扰,工程上更严谨,也能体现 ES6 基础。
Q4:这道题的时间复杂度能优化到低于 O(n) 吗?
A4:不能,因为要找到和为 target 的两个数,至少需要遍历一次数组,因此时间复杂度的下界是 O(n)。
2. 新手常见避坑点
暴力法中内层循环从 0 开始,导致重复使用元素或无效循环;
用普通对象做哈希表时,直接用 if (map[complement]) 判断补数是否存在,忽略 0 等假值的情况;
哈希表法中「先存入当前元素,再判断补数」,导致补数是当前元素本身,违反题目要求;
忽略复杂度分析,面试中无法说清两种解法的优劣。