两数之和
题目描述:
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
示例 1:
ini
输入: nums = [2,7,11,15], target = 9
输出: [0,1]
解释: 因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
ini
输入: nums = [3,2,4], target = 6
输出: [1,2]
示例 3:
ini
输入: nums = [3,3], target = 6
输出: [0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
- 只会存在一个有效答案
暴力解法
-
思路:
- 使用两层嵌套循环遍历数组。外层循环从数组的第一个元素开始,内层循环从外层循环当前元素的下一个元素开始。对于每一对元素,计算它们的和,并检查是否等于目标值
target
。如果相等,则返回这两个元素的下标。
- 使用两层嵌套循环遍历数组。外层循环从数组的第一个元素开始,内层循环从外层循环当前元素的下一个元素开始。对于每一对元素,计算它们的和,并检查是否等于目标值
-
代码实现:
ts
function twoSum(nums: number[], target: number): number[] {
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];
}
}
}
return [];
}
- 分析:
- 时间复杂度 :O(n2),因为有两层嵌套循环,外层循环执行
n
次,内层循环对于每次外层循环平均执行(n - 1) / 2
次,总体时间复杂度为 O(n×n)。 - 空间复杂度:O(1),除了存储结果的数组外,只使用了常数级别的额外空间。
- 缺点:时间复杂度较高,当数组长度较大时,运行效率很低。对于大规模数据,计算量会急剧增加。
哈希表解法
- 思路:
- 使用一个哈希表(在 TypeScript 中可以用对象或者Map模拟)来存储数组元素及其下标。遍历数组时,对于每个元素
nums[i]
,计算target - nums[i]
,然后检查这个差值是否已经在哈希表中。如果在哈希表中,则说明找到了两个数的和为target
,返回哈希表中差值对应的下标和当前下标i
。如果不在哈希表中,则将当前元素nums[i]
及其下标i
存入哈希表,继续遍历。
- 代码实现:
ts
//对象
function twoSum(nums: number[], target: number): number[] {
const hashMap: { [key: number]: number } = {};
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (complement in hashMap) {
return [hashMap[complement], i]
}
hashMap[nums[i]] = i;
}
return [];
}
// Map
function twoSum(nums: number[], target: number): number[] {
const hashMap = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (hashMap.has(complement)) {
return [hashMap.get(complement), i];
}
hashMap.set(nums[i], i);
}
return [];
}
-
分析:
- 时间复杂度:O(n),因为只需要遍历数组一次。每次在哈希表中查找和插入操作平均时间复杂度为 O(1)。
- 空间复杂度:O(n),在最坏情况下,哈希表需要存储数组中的所有元素。
-
优点:相比暴力解法,哈希表解法的时间复杂度大大降低,在处理大规模数据时效率更高。虽然空间复杂度为 O(n),但在实际应用中,时间效率的提升往往更为关键。
最优解及原因
- 最优解:哈希表解法是本题的最优解。
- 原因:在大多数情况下,尤其是处理大规模数据时,时间复杂度是衡量算法优劣的重要指标。哈希表解法将时间复杂度从暴力解法的 O(n2) 降低到了 O(n),这使得算法在运行时间上有了显著的提升。虽然哈希表解法需要额外的 O(n) 空间,但以空间换取时间的方式在很多场景下是值得的。
拓展和题目变形
1.拓展:
-
如果需要返回所有满足和为
target
的二元组,而不只是一组解。 -
思路:可以使用一个数组来存储所有符合条件的二元组。在哈希表解法的基础上,每次找到符合条件的二元组时,将其添加到结果数组中。
-
代码实现:
ts
function twoSumAllPairs(nums: number[], target: number): number[][] {
const hashMap: { [key: number]: number } = {};
const result: number[][] = [];
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (complement in hashMap) {
result.push([hashMap[complement], i]);
}
hashMap[nums[i]] = i;
}
return result;
}
- 题目变形:
-
变形一 :如果数组中可能存在重复元素,且要求返回所有满足和为
target
的二元组,包括重复元素组成的二元组。 -
思路:在上述返回所有二元组的代码基础上,修改哈希表的存储方式,使其可以处理重复元素。可以将哈希表的值改为数组,存储所有相同元素的下标。
-
代码实现:
ts
function twoSumAllPairsWithDuplicates(nums: number[], target: number): number[][] {
const hashMap: { [key: number]: number[] } = {};
const result: number[][] = [];
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (complement in hashMap) {
for (const index of hashMap[complement]) {
result.push([index, i]);
}
}
if (!hashMap[nums[i]]) {
hashMap[nums[i]] = [];
}
hashMap[nums[i]].push(i);
}
return result;
}
-
变形二 :给定一个有序数组
nums
,找到两个数使得它们的和最接近目标值target
。 -
思路:使用双指针法。初始化两个指针,一个指向数组开头,一个指向数组末尾。计算两个指针所指元素的和,与目标值比较。如果和等于目标值,直接返回;如果和大于目标值,将右指针左移;如果和小于目标值,将左指针右移。每次移动指针后,更新最接近目标值的和及对应的两个数。
-
代码实现:
ts
function twoSumClosest(nums: number[], target: number): number[] {
let left = 0;
let right = nums.length - 1;
let closestSum = nums[left] + nums[right];
let result: number[] = [nums[left], nums[right]];
while (left < right) {
const sum = nums[left] + nums[right];
if (sum === target) {
return [nums[left], nums[right]];
} else if (sum > target) {
right--;
} else {
left++;
}
if (Math.abs(sum - target) < Math.abs(closestSum - target)) {
closestSum = sum;
result = [nums[left], nums[right]];
}
}
return result;
}