#两数之和
#题目描述
想象你手中有一串随机排列的数字。我们的目标很简单:从这串数字中挑选出两个,让它们的和恰好等于一个给定的目标值。这听起来像是在杂乱的抽屉里寻找一双颜色匹配的袜子——你不仅要找到这两个数字,还需要记下它们在原始队列中所在的位置。为了让逻辑更清晰,我们约定在一次寻找过程中,每个数字只能参与一次配对,且题目保证这组数字中一定存在且仅存在一组完美的搭配。
#思路分析
最直白的想法莫过于“地毯式搜索”:我们先固定住第一个数字,然后让剩下的每一个数字与其尝试相加。如果失败了,我们就换下一个数字继续尝试。这种双重循环的暴力解法虽然直观得像手工清点,但当数字成千上万时,重复的遍历会让整个过程变得沉重且低效。
为了变得更聪明,我们可以试着建立一份“记忆”。当我们看到一个数字时,立刻就能算出它匹配成功所需的“另一半”应该是多少。接着,我们只需在一个随身携带的清单中查找这个“另一半”是否出现过。如果在那儿,说明我们找到了失散的搭档;如果不在,就把当前的数字和它的位置记在清单上,留给后来的数字查看。这种方案利用哈希表(Map)将“漫长的寻找”变成了“瞬间的相遇”,在牺牲极小额外存储空间的情况下,极大地提升了处理的速度。
#解法
哈希表解法
暴力解法
/**
* 两数之和:哈希表解法
* 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的两个整数,并返回它们的数组下标。
*
* 时间复杂度: O(n) - 只需要遍历一次数组,哈希表的查找和插入操作平均时间复杂度为 O(1)。
* 空间复杂度: O(n) - 在最坏情况下需要存储所有数组元素。
*
* @param {number[]} nums - 输入的整数数组
* @param {number} target - 目标和
* @returns {number[]} - 返回两个数字的下标数组,如果未找到则返回空数组 []
*/
export function twoSum(nums, target) {
// 1. 参数校验:如果数组不存在、不是数组或者长度不足 2(至少需要两个数才能求和)
if (!nums || !Array.isArray(nums) || nums.length < 2) {
return [];
}
// 2. 创建一个 Map 用来存储已经遍历过的数字及其索引
// Key: 数字的值, Value: 该数字在原数组中的下标 (Index)
const hashMap = new Map();
// 3. 遍历数组
for (let i = 0; i < nums.length; i++) {
// 计算当前数字想要达成目标值所需要的“另一半”(也就是补数)
const diff = target - nums[i];
// 4. 检查补数是否已经在哈希表中
// 如果在,说明我们之前遇到过它,它的下标 + 当前下标 i 就是结果
if (hashMap.has(diff)) {
// 返回保存的下标和当前下标
return [hashMap.get(diff), i];
}
// 5. 如果补数不在哈希表里,将当前数字及其下标存入哈希表
// 这样后面遍历到的数字如果需要它,就能快速找到
hashMap.set(nums[i], i);
}
// 6. 如果遍历完都没有找到结果,按照约定返回空数组
return [];
}
// --- 测试用例 (Test Cases) ---
/**
* 运行示例
*/
// 示例 1: 标准情况
// nums = [2, 7, 11, 15], target = 9
// 2 + 7 = 9, 返回 [0, 1]
console.log('Test 1:', twoSum([2, 7, 11, 15], 9));
// 示例 2: 目标值由后面的数字组成
// nums = [3, 2, 4], target = 6
// 2 + 4 = 6, 返回 [1, 2]
console.log('Test 2:', twoSum([3, 2, 4], 6));
// 示例 3: 两个相同的数字相加
// nums = [3, 3], target = 6
// 3 + 3 = 6, 返回 [0, 1]
console.log('Test 3:', twoSum([3, 3], 6));
// 示例 4: 无解情况
console.log('Test 4 (No solution):', twoSum([1, 2, 3], 7));
// 示例 5: 边界情况(空数组或长度不足)
console.log('Test 5 (Empty/Short):', twoSum([1], 9));
/**
* 两数之和:暴力搜索解法 (Double For Loop)
* 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的两个整数,并返回它们的数组下标。
*
* 时间复杂度: O(n^2) - 嵌套的双重循环,外层循环运行 n 次,内层循环平均运行 n/2 次。
* 空间复杂度: O(1) - 只需要常数级的额外空间存储循环变量。
*
* 优点:写法最直观,不需要额外的内存(如 Map)。
* 缺点:当数组规模很大时,运行效率非常低。
*
* @param {number[]} nums - 输入的整数数组
* @param {number} target - 目标和
* @returns {number[]} - 返回两个数字的下标数组,如果未找到则返回空数组 []
*/
export function twoSum(nums, target) {
// 1. 参数校验:确保 nums 存在且至少有两个元素,否则无法求和
if (!nums || !Array.isArray(nums) || nums.length < 2) {
return [];
}
// 2. 外层循环:遍历每一个数作为“第一个数”
for (let i = 0; i < nums.length; i++) {
// 计算当前数字想要达成目标值所需要的“另一半” (补数)
const diff = target - nums[i];
// 3. 内层循环:从第一个数后面的数字中查找是否存在匹配的补数
// 注意这里 j = i + 1,是为了避免重复使用同一个数字
for (let j = i + 1; j < nums.length; j++) {
// 4. 检查:如果当前数字正好等于我们正在找的补数
if (diff === nums[j]) {
// 5. 找到匹配!返回这两个数的下标组成的数组
return [i, j];
}
}
}
// 6. 如果所有循环结束都没有找到结果,返回空数组
return [];
}
// --- 测试用例 (Test Cases) ---
/**
* 运行示例
*/
// 示例 1: 标准情况
// nums = [2, 7, 11, 15], target = 9
// 2 + 7 = 9, 返回 [0, 1]
console.log('Test 1:', twoSum([2, 7, 11, 15], 9));
// 示例 2: 目标值由后面的数字组成
// nums = [3, 2, 4], target = 6
// 2 + 4 = 6, 返回 [1, 2]
console.log('Test 2:', twoSum([3, 2, 4], 6));
// 示例 3: 两个相同的数字相加
// nums = [3, 3], target = 6
// 3 + 3 = 6, 返回 [0, 1]
console.log('Test 3:', twoSum([3, 3], 6));
// 示例 4: 无解情况
console.log('Test 4 (No solution):', twoSum([1, 2, 3], 7));
// 示例 5: 边界情况(空数组或长度不足)
console.log('Test 5 (Empty/Short):', twoSum([1], 9));