目录
- 1.问题描述
- 2.解题思路全解析
-
- [2.1 思路一:暴力枚举法(最直观)](#2.1 思路一:暴力枚举法(最直观))
- [2.2 思路二:哈希表法(最优解)](#2.2 思路二:哈希表法(最优解))
- [2.3 思路三:排序+双指针法(特殊情况)](#2.3 思路三:排序+双指针法(特殊情况))
- 3.深入理解:算法背后的思想
- 4.性能对比与选择策略
-
- [4.1 性能对比](#4.1 性能对比)
- [4.2 选择策略建议](#4.2 选择策略建议)
- 5.扩展问题与变体
-
- [5.1 变体1:三数之和](#5.1 变体1:三数之和)
- [5.2 变体2:四数之和](#5.2 变体2:四数之和)
- [5.3 变体3:两数之和II(输入有序数组)](#5.3 变体3:两数之和II(输入有序数组))
- 6.常见错误与陷阱
-
- [6.1 陷阱1:重复使用同一元素](#6.1 陷阱1:重复使用同一元素)
- [6.2 陷阱2:未考虑负数情况](#6.2 陷阱2:未考虑负数情况)
- [6.3 陷阱3:处理边界条件](#6.3 陷阱3:处理边界条件)
- 7.总结
1.问题描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。
难度:简单
示例1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9,所以返回 [0, 1]
示例2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例3:
输入:nums = [3,3], target = 6
输出:[0,1]
2.解题思路全解析
2.1 思路一:暴力枚举法(最直观)
核心思想
遍历所有可能的元素组合,检查它们的和是否等于目标值。
算法步骤
- 使用两层循环,外层循环遍历每个元素作为第一个加数
- 内层循环遍历外层循环之后的元素作为第二个加数
- 检查两数之和是否等于目标值
- 找到后立即返回结果
时间复杂度分析
- 最坏情况:O(n²),需要检查所有n(n-1)/2种组合
- 最好情况:O(1),前两个元素就是解
- 平均情况:O(n²)
空间复杂度分析
- O(1),只使用了常数级别的额外空间
代码实现
java
public class BruteForceSolution {
public int[] twoSum(int[] nums, int target) {
// 遍历所有可能的组合
for (int i = 0; i < nums.length; i++) {
// j从i+1开始,避免重复使用同一元素
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
// 根据题意,总会有一个解,这里仅为编译需要
return new int[0];
}
}
适用场景
- 数据规模较小(n ≤ 1000)
- 内存极其受限的环境
- 对代码简洁性要求高,不追求极致性能
2.2 思路二:哈希表法(最优解)
核心思想
利用哈希表(HashMap)实现O(1)时间复杂度的查找,将问题从"寻找两数"转化为"寻找一个数的补数"。
算法原理
对于数组中的每个元素 nums[i],我们需要找到的是它的补数 complement = target - nums[i]。如果这个补数已经出现过,我们就找到了答案。
关键点
- 边遍历边存储:在遍历过程中将元素存入哈希表
- 查找补数:对于当前元素,查找它的补数是否已在哈希表中
- 避免重复使用:先查找再存储,确保不会重复使用同一元素
算法步骤
- 创建HashMap,键为元素值,值为元素索引
- 遍历数组中的每个元素
nums[i] - 计算补数
complement = target - nums[i] - 检查补数是否在HashMap中
- 如果在,返回
[map.get(complement), i] - 如果不在,将当前元素存入HashMap:
map.put(nums[i], i)
- 如果在,返回
时间复杂度分析
- 平均情况:O(n),每个元素只需处理一次
- 查找操作:HashMap的查找平均为O(1)
空间复杂度分析
- O(n),最坏情况下需要存储所有元素
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class HashMapSolution {
public int[] twoSum(int[] nums, int target) {
// 创建哈希表存储元素值和索引的映射
Map<Integer, Integer> numMap = new HashMap<>();
// 遍历数组
for (int i = 0; i < nums.length; i++) {
// 计算当前元素所需的补数
int complement = target - nums[i];
// 检查补数是否已在哈希表中
if (numMap.containsKey(complement)) {
// 找到解,返回两个索引
return new int[]{numMap.get(complement), i};
}
// 将当前元素存入哈希表(放在查找之后,避免重复使用同一元素)
numMap.put(nums[i], i);
}
}
}
为什么这种方法是正确的?
关键在于处理顺序:对于每个元素,我们先检查它的补数是否已经出现过,然后再将自己加入哈希表。这样确保了:
- 不会重复使用同一个元素
- 不会漏掉任何可能的组合
- 只需要遍历一次数组
性能优化技巧
java
public class OptimizedHashMapSolution {
public int[] twoSum(int[] nums, int target) {
// 预先设置HashMap的初始容量,避免扩容开销
Map<Integer, Integer> map = new HashMap<>(nums.length);
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
// 直接使用get方法检查,减少一次containsKey调用
Integer index = map.get(complement);
if (index != null) {
return new int[]{index, i};
}
map.put(nums[i], i);
}
return new int[0];
}
}
2.3 思路三:排序+双指针法(特殊情况)
适用场景
这种方法不直接适用于本题,因为排序会改变元素的原始索引。但如果问题变形为只需要返回数值,或者允许额外空间存储索引信息,这是一种高效的解法。
核心思想
- 将数组排序
- 使用两个指针从两端向中间移动
- 根据两数之和与目标值的比较,决定移动哪个指针
算法步骤
- 创建数组副本并排序,同时记录原始索引
- 初始化两个指针:left = 0, right = n-1
- 当 left < right 时循环:
- 计算 sum = nums[left] + nums[right]
- 如果 sum == target,返回原始索引
- 如果 sum < target,left++(需要更大的数)
- 如果 sum > target,right--(需要更小的数)
代码实现(返回数值版)
java
import java.util.Arrays;
public class TwoPointerSolution {
// 返回数值而不是索引的版本
public int[] twoSumValues(int[] nums, int target) {
// 先对数组排序
int[] sortedNums = nums.clone();
Arrays.sort(sortedNums);
int left = 0, right = sortedNums.length - 1;
while (left < right) {
int sum = sortedNums[left] + sortedNums[right];
if (sum == target) {
return new int[]{sortedNums[left], sortedNums[right]};
} else if (sum < target) {
left++;
} else {
right--;
}
}
return new int[0];
}
// 如果需要返回索引,需要额外处理
public int[] twoSumWithIndices(int[] nums, int target) {
// 创建索引数组
Integer[] indices = new Integer[nums.length];
for (int i = 0; i < nums.length; i++) {
indices[i] = i;
}
// 根据数值对索引排序
Arrays.sort(indices, (a, b) -> Integer.compare(nums[a], nums[b]));
int left = 0, right = nums.length - 1;
while (left < right) {
int sum = nums[indices[left]] + nums[indices[right]];
if (sum == target) {
return new int[]{indices[left], indices[right]};
} else if (sum < target) {
left++;
} else {
right--;
}
}
return new int[0];
}
}
复杂度分析
- 时间复杂度:O(n log n),主要来自排序
- 空间复杂度:O(n),需要存储索引信息
3.深入理解:算法背后的思想
-
空间换时间思想
哈希表解法完美体现了"空间换时间"的设计思想。通过使用O(n)的额外空间,我们将时间复杂度从O(n²)降低到O(n)。
-
问题转化思想
将"寻找两个数"转化为"寻找一个数的补数",这是算法设计中的关键思维转换。
-
遍历顺序的重要性
先查找后存储的顺序,确保了不会重复使用同一个元素,这是哈希表解法正确性的关键。
4.性能对比与选择策略
4.1 性能对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 暴力枚举 | O(n²) | O(1) | 小规模数据 | 简单直观,内存占用少 | 性能差,不适用于大数据 |
| 哈希表 | O(n) | O(n) | 大规模数据 | 性能最优,代码简洁 | 需要额外内存 |
| 双指针 | O(n log n) | O(1)或O(n) | 已排序或可排序数据 | 空间效率高 | 需要排序,改变索引 |
4.2 选择策略建议
- 面试场景:首选哈希表解法,展现对数据结构和时间复杂度的理解
- 竞赛场景:根据数据规模选择,通常哈希表最稳妥
- 生产环境 :
- 如果内存充足,选择哈希表
- 如果内存紧张但数据有序,考虑双指针
- 如果数据量很小,暴力法可能更简单
5.扩展问题与变体
5.1 变体1:三数之和
在"两数之和"的基础上,可以扩展到"三数之和"问题。
java
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums == null || nums.length < 3) return result;
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
// 跳过重复元素
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 跳过重复元素
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
5.2 变体2:四数之和
进一步扩展,可以使用类似的思路解决四数之和问题。
5.3 变体3:两数之和II(输入有序数组)
如果输入数组已经排序,可以使用双指针法达到O(n)时间复杂度和O(1)空间复杂度。
java
public int[] twoSumSorted(int[] numbers, int target) {
int left = 0, right = numbers.length - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) {
return new int[]{left + 1, right + 1}; // 题目要求从1开始计数
} else if (sum < target) {
left++;
} else {
right--;
}
}
return new int[0];
}
6.常见错误与陷阱
6.1 陷阱1:重复使用同一元素
java
// 错误示例
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < nums.length; j++) { // j从0开始,可能重复使用同一元素
if (i != j && nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
6.2 陷阱2:未考虑负数情况
哈希表解法天然支持负数,但某些特殊实现可能忽略了这一点。
6.3 陷阱3:处理边界条件
- 空数组或单个元素的数组
- 不存在解的情况(虽然题目假设总有一个解)
7.总结
两数之和问题虽然简单,但它像一面镜子,反映了一个程序员的算法思维和编码能力。通过这道题,我们学习到了:
- 暴力解法:最简单直接,适用于小规模数据
- 哈希表解法:空间换时间的典范,是最优解
- 双指针解法:适用于有序数据的特殊情况
更重要的是,我们学会了如何分析问题、选择合适的数据结构、优化算法复杂度。这些技能不仅适用于两数之和问题,也是解决更复杂算法问题的基础。