题目要求
- 给定一个整数
nums和一个目标值target,在该数组中找出和为目标值的那两个整数并返回他们的数组下标; - 可以假设输出仅对应一个答案,但是是不能重复利用数组中的元素;
其中:
2 <= nums.length <= 1e4-1e9 <= nums[i] <= 1e9-1e9 <= target <= 1e9
方法一:遍历法(for循环)
基本思路
两层嵌套的for循环遍历所有可能的元素组合,元素组合中的两个元素变量分别设为i和j。其中:i从0到n-1,j从i到n-1,选组组合的种类数为:。代码通过循环逐个访问数组元素,并判断是否符合条件:nums[i] + nums[j] == target。由于数组的最大长度规定为1e4,平方后为1e8,其中与C++一秒能够执行的量级近似,从而大概率应该不会超时。且题目中规定了答案的唯一性,从而可以保证运行结果的两两组合的唯一性。
代码
class Solution{
public:
vector<int> twoSum(vector<int>& nums, int target){
int n = nums.size();
for (int i = 0; i < n; ++i){
for (int j = i+1; j < n; ++j){
if (nums[i] + nums[j] == target){
return {i, j};
}
}
}
return {};
}
};
复杂度分析
- 时间复杂度;是数组中的元素数量,最坏情况下数组中的任意两个不同下标的数都需要匹配一次。
- 空间复杂度;
几点注释
- C++标准序的
vector容器:代码使用vector<int>作为参数和返回值,涉及:vector的声明,元素访问nums[i],初始化return{i , j}和空间容器返回return{}。如:直接通过{i , j}构造并返回vector是C++11的列表初始化特征。 - 函数返回值与类成员函数:函数定义在
Solution类中,是面向对象编程的体现。返回类型为vector<int>,符合题目要求的格式。若未找到解,返回空vector{}。 - C++语法特征:列表初始化:
return {i, j}利用了C++11的统一初始化语法,简化代码。作用域限定:循环变量i和j的作用域限制在循环内部,符合现代C++的编码习惯。
方法二:双指针法(仅适用于有序数组的特定场景)
基本思路
由于双指针仅适用于有序数组的特定场景,因此在使用双指针法解决两数之和问题时,通常需要先将数组排序,然后利用左右指针向中间逼近的特性寻找符合条件的数对。但是需注意的是:双指针法直接应用在原始数组上会导致索引变化,因此在书写代码的同时还需要注意额外处理索引的记录。
首先保存原始索引,由于排序后索引会发生变化,此时需要将数值和索引一起储存。其次按数值的大小进行排序。最后进行双指针查找并返回相应整数的下标。代码
#include <vector>
#include <algorithm>
class Solution {
public:
std::vector<int> twoSum(std::vector<int>& nums, int target) {
// 1. 保存原始索引(排序后索引会变化)
std::vector<std::pair<int, int>> nums_with_index;
for (int i = 0; i < nums.size(); ++i) {
nums_with_index.emplace_back(nums[i], i);
}
// 2. 按值排序
std::sort(nums_with_index.begin(), nums_with_index.end());
// 3. 双指针查找
int left = 0, right = nums.size() - 1;
while (left < right) {
int sum = nums_with_index[left].first +nums_with_index[right].first;
if (sum == target) {
return {nums_with_index[left].second, nums_with_index[right].second};
}
else if (sum < target) {
left++;
}
else {
right--;
}
}
return {};
}
};
复杂度分析
- 时间复杂度:,其中:为排序时间复杂度;为双指针遍历时间复杂度。优于暴力解法的 。
- 空间复杂度;
几点注释
1.索引保存:排序会打乱原始索引,因此需要将值和索引一起存储(std::pair<int, int>)。
std::sort对数组进行排序,排序的时间复杂度为 。
3. 双指针逼近: 左指针 left 从数组起始位置开始,右指针 right 从数组末尾开始。根据当前两数之和与目标值的比较,决定移动左指针(和太小)或右指针(和太大)。
4. 双指针法更适用于类似“三数之和”等扩展问题。
方法三:哈希表解法(边遍历边查询)
基本思路
哈希算法是一种通过哈希函数将数据映射到固定大小的表中,以实现快速查找和插入的技术。其核心思想是用空间换时间,平均情况下可实现的时间复杂度。在解决两数之和问题时,哈希表被用来高效地存储和查询目标值与当前元素的差值。
首先对哈希表初始化unordered_map<int, int> hashtable:键(
int)存储数组元素的值,值(int)存储对应的索引。如:hashtable[3] = 0表示数值3的索引为0;然后遍历数组并查找目标差值,其中auto it = hashtable.find(target - nums[i])表示计算当前元素nums[i]的补数(即target - nums[i]),并在哈希表中查找是否存在该补数;if (it != hashtable.end())表示若找到补数,说明当前元素与其补数之和为target,直接返回两者的索引{it->second, i}。最后动态更新哈希表hashtable[nums[i]] = i:若未找到补数,将当前元素及其索引存入哈希表。这一操作保证了后续遍历时能及时查询到已处理过的元素。
代码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 1.哈希表:键为数值,值为索引
unordered_map<int, int> hashtable;
for (int i = 0; i < nums.size(); ++i) {
// 2.查找目标差值是否存在
auto it = hashtable.find(target - nums[i]);
// 3.找到则返回结果
if (it != hashtable.end()) {
//4.it->second是已存储的索引
return {it->second, i};
}
//5.未找到则将当前数值及索引存入哈希表
hashtable[nums[i]] = i;
}
//6.无解时返回空数组(题目保证有解,此行为语法需要)
return {};
}
};
复杂度分析
- 时间复杂度:每个元素仅遍历一次,哈希表的插入和查找操作平均为;
- 空间复杂度:最坏情况下需要存储所有元素。
几点注释
避免重复元素的处理方法:
由于哈希表在插入前先进行查询,即使数组中有重复元素,也能正确处理。例如,nums = [2, 2]且target = 4时,第二个2的补数(2)已存在于哈希表中,直接返回正确索引。
哈希算法的优势:通过哈希表将查找补数的时间复杂度优化至,实现了高效的线性时间复杂度。其核心在于边遍历边查询,既避免了重复元素的干扰,又减少了冗余计算,是典型空间换时间的策略
总结
| 方法 | 时间复杂度 | 空间复杂度 | 保留索引 | 处理重复元素 | 适用场景 |
|---|---|---|---|---|---|
| 哈希法 | 是 | 天然支持 | 数据无序且需要保留原始索引,对时间复杂度敏感,空间充足。 | ||
| 遍历法 | 是 | 支持 | 极小数据规模或验证正确性 | ||
| 双指针法 | 或 | 需额外操作 | 需手动跳过 | 数据可排序且对空间敏感 |
-
是否需要保留原始索引
- 哈希法、暴力法直接支持,双指针法需额外存储。
-
数据规模
- 大规模数据选哈希法,极小数据选暴力法,中等规模可考虑双指针。
-
数据是否有序
- 若已有序,双指针法更高效。
-
时间 vs. 空间权衡
- 时间优先选哈希法,空间优先选双指针法。
综上:
- 哈希法是解决两数之和的最优选择,兼顾时间效率和代码简洁性。
- 双指针法在空间敏感或数据有序时表现优异,但需牺牲索引或增加预处理成本。