题目描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
判题标准
系统会用以下代码来测试你的题解:
int[] nums = [...]; // 输入数组
int[] expectedNums = [...]; // 长度正确的期望答案
int target = ...; // 目标值
int[] result = twoSum(nums, target); // 调用
assert result.length == 2;
assert nums[result[0]] + nums[result[1]] == 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 <= nums.length <= 10^4
- -10^9 <= nums[i] <= 10^9
- -10^9 <= target <= 10^9
- 只会存在一个有效答案
进阶
你可以想出一个时间复杂度小于 O(n²) 的算法吗?
代码运行环境说明
本文档中的所有代码均基于 C++11 标准编写,需要包含以下头文件:
<vector>: 用于使用 std::vector 容器<unordered_map>: 用于使用 std::unordered_map 哈希表<algorithm>: 用于使用 std::find 等 STL 算法<utility>: 用于使用 std::pair 数据结构
编译时请确保使用支持 C++11 及以上标准的编译器,例如:
g++ -std=c++11 two-sum.cpp -o two-sum
解题思路
本题提供了多种解法,每种都有其独特的优势和适用场景。我们可以将这些解法分为以下几类:
- 算法本质实现:展示算法核心思想,不依赖STL库函数
- STL应用实现:利用STL库函数简化代码实现
- 其他特殊解法:展示不同的编程思想和技巧
解法比较与选择
算法本质实现 vs STL应用实现
-
算法本质实现:
- 优点:有助于理解算法核心思想,不依赖外部库,便于移植到其他语言
- 缺点:代码相对复杂,需要手动处理更多细节
- 适用场景:学习算法原理、面试手写代码、嵌入式等受限环境
-
STL应用实现:
- 优点:代码简洁,开发效率高,经过充分测试
- 缢点:依赖特定库,可能隐藏实现细节
- 适用场景:实际项目开发、快速原型开发
推荐解法
在实际应用中,推荐使用方法二(哈希表一次遍历),理由如下:
- 时间复杂度O(n),空间复杂度O(n),均为较优
- 只需要一次遍历,效率高
- 逻辑清晰,易于理解和实现
- 无边界漏洞,适用于各种边界情况
极端测试用例
为了验证各种解法在边界条件下的正确性和鲁棒性,我们整理了以下极端测试用例:
1. 最小数组
输入:nums = [1,2], target = 3
预期输出:[0,1]
说明:测试解法对最小数组的处理能力
2. 最大负数和最大正数
输入:nums = [-1000000000, 1000000000, 1], target = 0
预期输出:[0,1]
说明:测试解法对边界数值的处理能力
3. 相同元素
输入:nums = [3,3], target = 6
预期输出:[0,1]
说明:测试解法对相同元素的处理能力
4. 多个满足条件的组合
输入:nums = [1,2,3,4,5], target = 6
预期输出:[1,3] 或 [0,4]
说明:测试解法是否能正确返回一个有效答案
5. 最大数组长度
输入:nums = [1,2,3,...,10000], target = 19999
预期输出:[9998,9999]
说明:测试解法在最大输入规模下的性能和稳定性
6. 无解情况
输入:nums = [1,2,3], target = 10
预期输出:[]
说明:测试解法对无解情况的处理能力
测试建议
- 对于每种解法,都应该使用以上所有测试用例进行验证
- 特别关注边界值和相同元素这两种边界情况
- 对于递归解法,要注意测试大规模数据是否会引发栈溢出
- 对于STL相关解法,要验证其在极端输入下的行为是否符合预期
- 对于需要大量计算的解法(如暴力解法),要注意其在大规模数据下的性能表现
算法本质实现
这类解法展示了算法的核心思想,不依赖STL库函数,有助于理解算法本质。
方法一:暴力解法
// 【算法本质实现 - 双重循环】
std::vector<int> twoSum1(std::vector<int>& nums, int target) {
// 外层循环遍历数组中的每个元素,作为第一个加数
for (int i = 0; i < nums.size(); i++) {
// 内层循环从 i+1 开始遍历,避免重复计算和使用相同元素
// i之前的元素已与i匹配过,无需再次检查
for (int j = i + 1; j < nums.size(); j++) {
// 检查两个元素之和是否等于目标值
if (nums[i] + nums[j] == target) {
return {i, j};
}
}
}
return {};
}
- 所需头文件:
<vector> - 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 代码行数:6行
- 数据结构复杂度:无复杂数据结构
直接双重循环遍历所有可能的组合。
算法详解:
- 外层循环遍历数组中的每个元素
- 内层循环遍历当前元素之后的所有元素
- 检查两个元素之和是否等于目标值
- 如果找到匹配项,返回两个元素的索引
关键易错点:
- 内层循环应从 i+1 开始,避免使用相同元素两次
- 需要正确处理边界条件,确保不越界访问
推荐理由:
- 思路简单直接,易于理解
- 空间复杂度最优O(1)
- 适用于小数据集
方法二:哈希表(一次遍历)- 推荐解法
// 【算法本质实现 - 哈希表一次遍历】
std::vector<int> twoSum(std::vector<int>& nums, int target) {
// 创建一个哈希表用于存储已遍历的元素及其索引
std::unordered_map<int, int> map;
// 遍历数组中的每个元素
for (int i = 0; i < nums.size(); i++) {
// 在哈希表中查找当前元素的补数(target - nums[i])
auto iter = map.find(target - nums[i]);
// 如果找到补数,说明找到了两个和为target的元素
if (iter != map.end()) {
// 返回补数的索引和当前元素的索引
return {iter->second, i};
}
// 将当前元素及其索引存入哈希表,供后续元素查找
map[nums[i]] = i;
}
return {};
}
- 所需头文件:
<vector>,<unordered_map> - 时间复杂度:O(n)
- 空间复杂度:O(n)
- 代码行数:12行
- 数据结构复杂度:使用哈希表
边遍历边存储,同时查找是否存在匹配的元素。这是推荐在 LeetCode 上提交的解法。
算法详解:
- 创建一个哈希表用于存储已遍历的元素及其索引
- 遍历数组中的每个元素
- 对于当前元素,计算其补数(target - nums[i])
- 在哈希表中查找补数是否存在
- 如果存在,返回补数的索引和当前元素的索引
- 如果不存在,将当前元素及其索引存入哈希表
关键易错点:
- 必须先查找再插入,避免使用相同元素两次
- 返回结果时要注意索引顺序
推荐理由:
- 时间复杂度最优O(n)
- 逻辑清晰,易于实现
- 适用于各种规模的数据
方法三:双指针法
// 【算法本质实现 - 排序+双指针】
std::vector<int> twoSum3(std::vector<int>& nums, int target) {
// 创建索引和值的配对,保存原始索引信息
// 因为排序后元素位置会改变,需要保存原始索引
std::vector<std::pair<int, int>> indexed_nums;
for (int i = 0; i < nums.size(); i++) {
indexed_nums.push_back({nums[i], i});
}
// 按值排序,使数组有序,为双指针法创造条件
std::sort(indexed_nums.begin(), indexed_nums.end());
// 使用双指针分别指向数组的开头和结尾
int left = 0;
int right = indexed_nums.size() - 1;
// 当左指针小于右指针时继续循环
while (left < right) {
// 计算两个指针指向元素的和
int sum = indexed_nums[left].first + indexed_nums[right].first;
// 如果和等于目标值,找到答案
if (sum == target) {
// 返回对应的原始索引
return {indexed_nums[left].second, indexed_nums[right].second};
}
// 如果和小于目标值,左指针右移增大和
else if (sum < target) {
left++;
}
// 如果和大于目标值,右指针左移减小和
else {
right--;
}
}
return {};
}
- 所需头文件:
<vector>,<algorithm>,<utility> - 时间复杂度:O(n log n)
- 空间复杂度:O(n)
- 代码行数:26行
- 数据结构复杂度:使用pair和排序算法
需要先对数组排序,然后使用双指针技巧。注意这种方法需要保存原始索引信息。
算法详解:
- 创建一个包含值和原始索引的配对数组
- 按值对配对数组进行排序
- 使用双指针分别指向数组的开头和结尾
- 计算两个指针指向元素的和
- 如果和等于目标值,返回对应的原始索引
- 如果和小于目标值,左指针右移
- 如果和大于目标值,右指针左移
关键易错点:
- 需要保存原始索引信息,因为排序会改变元素位置
- 注意处理相同元素的情况
推荐理由:
- 展示了排序和双指针的结合应用
- 适用于已排序或可排序的数组
方法四:哈希表(两次遍历)
// 【算法本质实现 - 哈希表两次遍历】
std::vector<int> twoSum4(std::vector<int>& nums, int target) {
// 创建一个哈希表用于存储元素及其索引
std::unordered_map<int, int> map;
// 第一次遍历:构建完整的哈希表
for (int i = 0; i < nums.size(); i++) {
map[nums[i]] = i;
}
// 第二次遍历:查找目标值
for (int i = 0; i < nums.size(); i++) {
// 计算当前元素的补数
int complement = target - nums[i];
// 检查补数是否存在且不是当前元素本身
// map.count(complement)检查补数是否存在
// map[complement] != i 确保不使用相同元素两次
if (map.count(complement) && map[complement] != i) {
return {i, map[complement]};
}
}
return {};
}
- 所需头文件:
<vector>,<unordered_map> - 时间复杂度:O(n)
- 空间复杂度:O(n)
- 代码行数:18行
- 数据结构复杂度:使用哈希表
先建立完整的哈希表,然后再遍历查找。
算法详解:
- 第一次遍历数组,将所有元素及其索引存入哈希表
- 第二次遍历数组,对每个元素计算其补数
- 在哈希表中查找补数是否存在
- 如果存在且不是当前元素本身,返回两个元素的索引
关键易错点:
- 需要检查找到的元素不是当前元素本身
- 两次遍历增加了常数因子
推荐理由:
- 思路清晰,易于理解
- 适用于需要先处理完整数据再进行查询的情况
方法五:二分查找法
// 【算法本质实现 - 排序+二分查找】
std::vector<int> twoSum5(std::vector<int>& nums, int target) {
// 创建索引和值的配对,保存原始索引信息
std::vector<std::pair<int, int>> indexed_nums;
for (int i = 0; i < nums.size(); i++) {
indexed_nums.push_back({nums[i], i});
}
// 按值排序,为二分查找创造条件
std::sort(indexed_nums.begin(), indexed_nums.end());
// 对每个元素,使用二分查找寻找其补数
for (int i = 0; i < indexed_nums.size(); i++) {
// 计算当前元素的补数
int complement = target - indexed_nums[i].first;
// 设置二分查找的左右边界
int left = i + 1; // 从当前元素之后开始查找,避免使用相同元素
int right = indexed_nums.size() - 1;
// 二分查找过程
while (left <= right) {
// 计算中间位置,避免溢出的写法
int mid = left + (right - left) / 2;
// 如果找到补数,返回对应的原始索引
if (indexed_nums[mid].first == complement) {
return {indexed_nums[i].second, indexed_nums[mid].second};
}
// 如果中间值小于补数,调整左边界
else if (indexed_nums[mid].first < complement) {
left = mid + 1;
}
// 如果中间值大于补数,调整右边界
else {
right = mid - 1;
}
}
}
return {};
}
- 所需头文件:
<vector>,<algorithm>,<utility> - 时间复杂度:O(n log n)
- 空间复杂度:O(n)
- 代码行数:32行
- 数据结构复杂度:使用pair、排序和二分查找
先排序,然后对每个元素使用二分查找寻找其补数。
算法详解:
- 创建一个包含值和原始索引的配对数组
- 按值对配对数组进行排序
- 遍历排序后的数组
- 对每个元素,计算其补数
- 使用二分查找在剩余元素中寻找补数
- 如果找到,返回对应的原始索引
关键易错点:
- 二分查找的边界条件处理
- 需要保存原始索引信息
推荐理由:
- 展示了排序和二分查找的结合应用
- 适用于已排序或可排序的数组
方法六:暴力解法(变体)
// 【算法本质实现 - 双重循环变体】
std::vector<int> twoSum6(std::vector<int>& nums, int target) {
// 外层循环遍历数组中的每个元素
for (int i = 0; i < nums.size(); i++) {
// 内层循环从 i+1 开始遍历,避免重复计算和使用相同元素
for (int j = i + 1; j < nums.size(); j++) {
// 检查两个元素的和是否等于目标值
if (nums[i] + nums[j] == target) {
return {i, j};
}
}
}
return {};
}
- 所需头文件:
<vector> - 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 代码行数:9行
- 数据结构复杂度:无复杂数据结构
这是暴力解法的一个变体实现,与方法一本质上相同,只是分类不同。
算法详解:
- 使用双重循环遍历所有可能的元素对
- 检查每对元素的和是否等于目标值
- 如果找到匹配项,返回两个元素的索引
关键易错点:
- 内层循环应从 i+1 开始,避免使用相同元素两次
- 与方法一本质上相同,只是分类不同
推荐理由:
- 适用于教学演示不同分类方法
- 本质上与方法一相同
其他特殊解法
这些解法展示了不同的编程思想和技巧。
方法七:集合法
// 【特殊解法 - 集合+映射结合】
std::vector<int> twoSum7(std::vector<int>& nums, int target) {
// 创建一个集合用于快速检查元素是否存在
std::unordered_set<int> seen;
// 创建一个映射用于保存元素及其索引
std::unordered_map<int, int> index_map;
// 遍历数组中的每个元素
for (int i = 0; i < nums.size(); i++) {
// 计算当前元素的补数
int complement = target - nums[i];
// 检查补数是否已存在(在seen集合中)
if (seen.count(complement)) {
// 如果存在,返回补数的索引和当前元素的索引
return {index_map[complement], i};
}
// 将当前元素添加到集合和映射中
seen.insert(nums[i]);
index_map[nums[i]] = i;
}
return {};
}
- 所需头文件:
<vector>,<unordered_set>,<unordered_map> - 时间复杂度:O(n)
- 空间复杂度:O(n)
- 代码行数:19行
- 数据结构复杂度:同时使用集合和映射
使用set检查元素是否存在,同时用map保存索引信息。
算法详解:
- 创建一个集合用于检查元素是否存在
- 创建一个映射用于保存元素及其索引
- 遍历数组中的每个元素
- 对于当前元素,计算其补数
- 在集合中检查补数是否存在
- 如果存在,返回补数的索引和当前元素的索引
- 如果不存在,将当前元素添加到集合和映射中
关键易错点:
- 需要同时维护集合和映射两个数据结构
- 必须先查找再插入,避免使用相同元素两次
推荐理由:
- 展示了集合和映射的结合使用
- 适用于需要去重或检查元素存在性的情况
方法八:递归法
// 【特殊解法 - 递归实现】
std::vector<int> twoSum8(std::vector<int>& nums, int target) {
// 创建一个哈希表用于存储已遍历的元素及其索引
std::unordered_map<int, int> map;
// 调用递归辅助函数开始处理
return twoSumRecursive(nums, target, 0, map);
}
std::vector<int> twoSumRecursive(std::vector<int>& nums, int target, int index, std::unordered_map<int, int>& map) {
// 基本情况:如果索引超出数组范围,说明未找到答案
if (index >= nums.size()) {
return {};
}
// 计算当前元素的补数
int complement = target - nums[index];
// 在哈希表中查找补数是否存在
if (map.count(complement)) {
// 如果存在,返回补数的索引和当前元素的索引
return {map[complement], index};
}
// 将当前元素及其索引存入哈希表
map[nums[index]] = index;
// 递归处理下一个元素
return twoSumRecursive(nums, target, index + 1, map);
}
- 所需头文件:
<vector>,<unordered_map> - 时间复杂度:O(n)
- 空间复杂度:O(n) - 递归栈空间
- 代码行数:23行(包括递归函数)
- 数据结构复杂度:使用哈希表和递归调用栈
使用递归实现哈希表一次遍历的逻辑。
算法详解:
- 创建一个哈希表用于存储已遍历的元素及其索引
- 使用递归函数遍历数组中的每个元素
- 对于当前元素,计算其补数
- 在哈希表中查找补数是否存在
- 如果存在,返回补数的索引和当前元素的索引
- 如果不存在,将当前元素及其索引存入哈希表
- 递归处理下一个元素
关键易错点:
- 递归终止条件是索引超出数组范围
- 递归深度与数组长度成正比,可能导致栈溢出
递归深度风险:
- 递归深度与数组长度成正比,最深可达O(n)
- 当数组长度接近或超过系统栈深度时(如题目提示中的10⁴上限),会触发栈溢出,导致程序崩溃
- 在实际应用中,对于大规模数据,应避免使用递归解法
- 这是递归解法的主要限制,也是不推荐在生产环境中使用的原因
推荐理由:
- 展示了递归思维
- 适用于教学演示递归技巧
方法九:反向遍历哈希表法
// 【特殊解法 - 反向遍历】
std::vector<int> twoSum9(std::vector<int>& nums, int target) {
// 创建一个哈希表用于存储已遍历的元素及其索引
std::unordered_map<int, int> map;
// 从数组末尾开始向前遍历(反向遍历)
for (int i = nums.size() - 1; i >= 0; i--) {
// 在哈希表中查找当前元素的补数
auto iter = map.find(target - nums[i]);
// 如果找到补数,说明找到了两个和为target的元素
if (iter != map.end()) {
// 返回当前元素的索引和补数的索引
return {i, iter->second};
}
// 将当前元素及其索引存入哈希表
map[nums[i]] = i;
}
return {};
}
- 所需头文件:
<vector>,<unordered_map> - 时间复杂度:O(n)
- 空间复杂度:O(n)
- 代码行数:15行
- 数据结构复杂度:使用哈希表
从数组末尾开始遍历构建哈希表并查找。
算法详解:
- 创建一个哈希表用于存储已遍历的元素及其索引
- 从数组末尾开始向前遍历
- 对于当前元素,计算其补数
- 在哈希表中查找补数是否存在
- 如果存在,返回当前元素的索引和补数的索引
- 如果不存在,将当前元素及其索引存入哈希表
关键易错点:
- 遍历方向与常规方法相反
- 返回结果时要注意索引顺序
推荐理由:
- 展示了不同遍历顺序的实现
- 适用于特定遍历顺序需求
方法十:滑动窗口法
// 【特殊解法 - 滑动窗口】
std::vector<int> twoSum10(std::vector<int>& nums, int target) {
// 从窗口大小为2开始,逐步增加窗口大小
// 窗口大小为1无法找到两个数,所以从2开始
for (int window_size = 2; window_size <= nums.size(); window_size++) {
// 对于每个窗口大小,遍历所有可能的窗口位置
for (int i = 0; i <= nums.size() - window_size; i++) {
// 检查窗口内是否有两个数之和等于target
// j从窗口起始位置开始
for (int j = i; j < i + window_size - 1; j++) {
// k从j+1开始,确保不重复检查同一对元素
for (int k = j + 1; k < i + window_size; k++) {
// 如果找到和等于目标值的元素对,返回它们的索引
if (nums[j] + nums[k] == target) {
return {j, k};
}
}
}
}
}
return {};
}
- 所需头文件:
<vector> - 时间复杂度:O(n⁴)
- 空间复杂度:O(1)
- 代码行数:18行
- 数据结构复杂度:四重循环,无复杂数据结构
通过改变窗口大小和位置来查找满足条件的两个数。
算法详解:
- 从窗口大小为2开始,逐步增加窗口大小
- 对于每个窗口大小,遍历所有可能的窗口位置
- 在每个窗口内,检查所有可能的元素对
- 如果找到和等于目标值的元素对,返回它们的索引
关键易错点:
- 时间复杂度较高,不适用于大数据集
- 多层循环增加了实现复杂度
推荐理由:
- 展示了滑动窗口思想
- 适用于教学演示不同算法思想
方法十一:值范围映射数组法
// 【特殊解法 - 数组映射】
std::vector<int> twoSum11(std::vector<int>& nums, int target) {
// 由于数值范围很大,这种方法在实际中并不适用
// 但为了演示思想,我们假设数值范围较小
const int OFFSET = 100000; // 偏移量,处理负数,将负数映射到非负索引
const int MAX_VAL = 200000; // 最大可能值
// 创建映射数组,记录每个值最后出现的位置
// 数组索引对应数值+offset后的值,数组值对应原数组中的索引
std::vector<int> valueToIndex(MAX_VAL + 1, -1);
// 遍历原数组
for (int i = 0; i < nums.size(); i++) {
// 计算当前元素的补数,并加上偏移量使其非负
int complement = target - nums[i] + OFFSET;
// 检查补数是否在有效范围内且已存在
if (complement >= 0 && complement <= MAX_VAL && valueToIndex[complement] != -1) {
// 如果存在,返回补数的索引和当前元素的索引
return {valueToIndex[complement], i};
}
// 将当前元素值加上偏移量后存入映射数组
valueToIndex[nums[i] + OFFSET] = i;
}
return {};
}
- 所需头文件:
<vector> - 时间复杂度:O(n)
- 空间复杂度:O(范围)
- 代码行数:21行
- 数据结构复杂度:使用大数组进行映射
使用数组作为哈希表的替代,通过值映射到数组索引来实现快速查找。
算法详解:
- 创建一个足够大的数组,用于存储值到索引的映射
- 通过偏移量处理负数,将所有值映射到非负范围
- 遍历数组,对每个元素计算其补数
- 检查补数在映射数组中是否存在
- 如果存在,返回补数的索引和当前元素的索引
- 如果不存在,将当前元素及其索引存入映射数组
关键易错点:
- 需要处理负数,通过偏移量将其映射到非负范围
- 数组大小必须足够大以容纳所有可能的值
- 当数值范围很大时,这种方法会消耗大量内存
推荐理由:
- 展示了数组作为哈希表替代的思路
- 适用于数值范围较小的情况
- 在某些特定场景下可能比哈希表更快
局限性:
- 当数值范围很大时,会消耗大量内存
- 不适用于数值范围未知或过大的情况
- 在本题中,由于数值范围达到2×10^9,这种方法不实用
方法十二:逆向遍历 + 临时缓存法
// 【特殊解法 - 逆向遍历+缓存】
std::vector<int> twoSum12(std::vector<int>& nums, int target) {
// 创建一个哈希表作为临时缓存
std::unordered_map<int, int> cache;
// 从后向前遍历数组(逆向遍历)
for (int i = nums.size() - 1; i >= 0; i--) {
// 计算当前元素的补数
int complement = target - nums[i];
// 检查补数是否已在缓存中
if (cache.count(complement)) {
// 注意返回顺序,确保索引小的在前
// cache[complement]是补数的索引,一定小于当前索引i
return {i, cache[complement]};
}
// 将当前元素存入缓存
cache[nums[i]] = i;
}
return {};
}
- 所需头文件:
<vector>,<unordered_map> - 时间复杂度:O(n)
- 空间复杂度:O(n)
- 代码行数:19行
- 数据结构复杂度:使用哈希表
从后向前遍历数组,使用临时缓存存储已遍历的元素。
算法详解:
- 创建一个哈希表作为临时缓存
- 从数组末尾开始向前遍历
- 对于当前元素,计算其补数
- 在缓存中查找补数是否存在
- 如果存在,返回当前元素索引和补数索引(注意索引顺序)
- 如果不存在,将当前元素及其索引存入缓存
关键易错点:
- 遍历方向与常规方法相反
- 返回结果时需要注意索引顺序,确保小索引在前
- 与方法九的区别在于返回索引的顺序处理
推荐理由:
- 展示了不同遍历方向的实现
- 适用于特定场景或面试中的思维拓展
方法十三:差值标记法
// 【特殊解法 - 差值预存】
std::vector<int> twoSum13(std::vector<int>& nums, int target) {
// 创建一个映射,存储差值到索引的映射
// 与常规方法不同,这里存储的是target-nums[i]而不是nums[i]
std::unordered_map<int, int> diffMap;
// 遍历数组中的每个元素
for (int i = 0; i < nums.size(); i++) {
// 检查当前元素是否是之前某个元素需要的差值
// 这与常规方法的思路相反:常规方法是找target-nums[i]是否存在
// 这里是检查nums[i]是否是之前存储的某个差值
if (diffMap.count(nums[i])) {
// 如果是,返回之前元素的索引和当前元素的索引
return {diffMap[nums[i]], i};
}
// 将当前元素需要的差值存入映射
// 存储的是差值(target-nums[i])到索引的映射
int diff = target - nums[i];
diffMap[diff] = i;
}
return {};
}
- 所需头文件:
<vector>,<unordered_map> - 时间复杂度:O(n)
- 空间复杂度:O(n)
- 代码行数:20行
- 数据结构复杂度:使用哈希表
通过预先计算并存储每个元素需要的差值来实现查找。
算法详解:
- 创建一个哈希表用于存储差值到索引的映射
- 遍历数组中的每个元素
- 检查当前元素是否是之前某个元素需要的差值
- 如果是,返回之前元素的索引和当前元素的索引
- 如果不是,计算当前元素需要的差值并存入哈希表
关键易错点:
- 存储的是差值而不是元素值本身
- 与常规哈希表方法的思维角度不同
- 需要正确处理查找和存储的逻辑关系
推荐理由:
- 提供了不同的思维角度来解决问题
- 展示了逆向思维在算法设计中的应用
- 逻辑与常规哈希表方法等价,但表达方式不同
方法十四:位运算模拟加法
// 【特殊解法 - 位运算实现】
// 辅助函数:使用位运算实现两个整数相加
// 这种方法模拟了CPU进行加法运算的底层原理
int bitAdd(int a, int b) {
// 当进位为0时,加法运算完成
while (b != 0) {
// 计算进位:两个位都为1时产生进位
int carry = (a & b) << 1;
// 不带进位的加法:异或运算相当于不考虑进位的加法
a = a ^ b;
// 更新进位,继续下一轮运算
b = carry;
}
return a;
}
std::vector<int> twoSum14(std::vector<int>& nums, int target) {
// 使用暴力解法遍历所有元素对
for (int i = 0; i < nums.size(); i++) {
for (int j = i + 1; j < nums.size(); j++) {
// 使用位运算模拟加法来检查两数之和
// bitAdd函数替代了普通的加法运算符+
if (bitAdd(nums[i], nums[j]) == target) {
return {i, j};
}
}
}
return {};
}
- 所需头文件:
<vector> - 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 代码行数:24行(包括辅助函数)
- 数据结构复杂度:使用位运算,无复杂数据结构
使用位运算来模拟整数加法操作,然后进行比较。
算法详解:
- 实现一个辅助函数bitAdd,使用位运算模拟两个整数的加法
- 在bitAdd函数中,使用异或运算(^)计算不带进位的加法
- 使用与运算(&)和左移运算(<<)计算进位
- 重复这个过程直到没有进位为止
- 在主函数中使用暴力解法遍历所有元素对
- 使用bitAdd函数计算两个元素的和,并与目标值比较
关键易错点:
- 位运算加法的实现需要正确处理进位
- 该方法的时间复杂度与暴力解法相同,只是加法操作的实现方式不同
- 位运算在某些情况下可能比普通加法稍快,但在这个问题中优势不明显
推荐理由:
- 展示了位运算在算术运算中的应用
- 适用于教学演示位运算技巧
- 提供了不同的实现思路
STL应用实现
这类解法利用STL库函数简化代码实现,适合在实际项目中快速解决问题。
方法十五:STL find函数法
// 【STL应用实现 - find函数】
std::vector<int> twoSum15(std::vector<int>& nums, int target) {
// 遍历数组中的每个元素
for (int i = 0; i < nums.size(); i++) {
// 使用STL的find函数在剩余元素中查找补数
// 搜索范围从当前元素之后开始,避免使用相同元素
auto it = std::find(nums.begin() + i + 1, nums.end(), target - nums[i]);
// 如果找到补数(迭代器不等于end())
if (it != nums.end()) {
// 计算找到元素的索引
// 使用std::distance计算迭代器距离起始位置的距离
int j = std::distance(nums.begin(), it);
// 返回两个元素的索引
return {i, j};
}
}
return {};
}
- 所需头文件:
<vector>,<algorithm> - 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 代码行数:15行
- 数据结构复杂度:使用STL算法函数
使用STL的find函数在剩余元素中查找补数。
算法详解:
- 遍历数组中的每个元素
- 对于当前元素,计算其补数
- 使用STL的find函数在当前元素之后的元素中查找补数
- 如果找到,计算其索引并返回两个元素的索引
关键易错点:
- find函数的搜索范围应从当前元素之后开始
- 需要使用std::distance计算迭代器距离以获取索引
推荐理由:
- 展示了STL算法的实际应用
- 代码简洁,易于理解
- 适用于教学演示STL使用
解法综合对比分析
为了更好地理解各种解法的特点和适用场景,我们对所有解法进行综合对比分析:
| 解法编号 | 解法名称 | 时间复杂度 | 空间复杂度 | 所需头文件 | 代码行数 | 数据结构复杂度 | 优缺点 | 适用场景 |
|---|---|---|---|---|---|---|---|---|
| 方法一 | 暴力解法 | O(n²) | O(1) | <vector> | 6行 | 无复杂数据结构 | 优点:思路简单,空间复杂度最优 缺点:时间复杂度高 | 小数据集(n < 100)或内存敏感环境 |
| 方法二 | 哈希表(一次遍历) | O(n) | O(n) | <vector>, <unordered_map> | 12行 | 使用哈希表 | 优点:时间复杂度最优,逻辑清晰 缺点:空间复杂度O(n) | 一般情况下的最优解,尤其适用于大数据集 |
| 方法三 | 双指针法 | O(n log n) | O(n) | <vector>, <algorithm>, <utility> | 26行 | 使用pair和排序算法 | 优点:展示了排序和双指针技巧 缺点:需要额外空间保存索引 | 数组已排序或可排序时 |
| 方法四 | 哈希表(两次遍历) | O(n) | O(n) | <vector>, <unordered_map> | 18行 | 使用哈希表 | 优点:思路清晰 缺点:需要两次遍历 | 需要先处理完整数据再查询时 |
| 方法五 | 二分查找法 | O(n log n) | O(n) | <vector>, <algorithm>, <utility> | 32行 | 使用pair、排序和二分查找 | 优点:展示了排序和二分查找技巧 缺点:需要额外空间保存索引 | 数组已排序或可排序时 |
| 方法六 | 暴力解法(变体) | O(n²) | O(1) | <vector> | 9行 | 无复杂数据结构 | 优点:思路简单 缺点:时间复杂度高 | 教学演示不同分类方法 |
| 方法七 | 集合法 | O(n) | O(n) | <vector>, <unordered_set>, <unordered_map> | 19行 | 同时使用集合和映射 | 优点:展示了集合和映射结合使用 缺点:需要维护两个数据结构 | 需要去重或检查元素存在性时 |
| 方法八 | 递归法 | O(n) | O(n) | <vector>, <unordered_map> | 23行 | 使用哈希表和递归调用栈 | 优点:展示了递归思维 缺点:递归深度风险 | 教学演示递归技巧 |
| 方法九 | 反向遍历哈希表法 | O(n) | O(n) | <vector>, <unordered_map> | 15行 | 使用哈希表 | 优点:展示了不同遍历顺序 缺点:无明显优势 | 特定遍历顺序需求 |
| 方法十 | 滑动窗口法 | O(n⁴) | O(1) | <vector> | 18行 | 四重循环,无复杂数据结构 | 优点:展示了滑动窗口思想 缺点:时间复杂度极高 | 教学演示不同算法思想 |
| 方法十一 | 值范围映射数组法 | O(n) | O(范围) | <vector> | 21行 | 使用大数组进行映射 | 优点:数组访问速度快 缺点:内存消耗大 | 数值范围较小的特定场景 |
| 方法十二 | 逆向遍历+临时缓存法 | O(n) | O(n) | <vector>, <unordered_map> | 19行 | 使用哈希表 | 优点:展示了不同遍历方向 缺点:无明显优势 | 特定遍历顺序需求 |
| 方法十三 | 差值标记法 | O(n) | O(n) | <vector>, <unordered_map> | 20行 | 使用哈希表 | 优点:提供了不同思维角度 缺点:思维转换成本高 | 教学演示逆向思维 |
| 方法十四 | 位运算模拟加法 | O(n²) | O(1) | <vector> | 24行 | 使用位运算,无复杂数据结构 | 优点:展示了位运算应用 缺点:时间复杂度高 | 教学演示位运算技巧 |
| 方法十五 | STL find函数法 | O(n²) | O(1) | <vector>, <algorithm> | 15行 | 使用STL算法函数 | 优点:使用STL算法,代码简洁 缺点:时间复杂度较高 | 教学演示STL使用 |
常见错误总结
在实现这些解法的过程中,新手容易犯一些常见错误,以下是主要的错误类型和避免方法:
1. 边界条件处理错误
错误示例:
// 错误:内层循环从j=0开始,可能导致使用相同元素两次
for (int i = 0; i < nums.size(); i++) {
for (int j = 0; j < nums.size(); j++) { // 错误的起始位置
if (i != j && nums[i] + nums[j] == target) {
return {i, j};
}
}
}
调试过程: 使用测试用例 [3,3],target=6 时,可能会返回 [0,0] 或其他错误结果。
正确做法:
// 正确:内层循环从j=i+1开始
for (int i = 0; i < nums.size(); i++) {
for (int j = i + 1; j < nums.size(); j++) { // 正确的起始位置
if (nums[i] + nums[j] == target) {
return {i, j};
}
}
}
避免方法:
- 仔细分析题目要求,确保不使用相同元素两次
- 使用具体例子手动执行代码验证逻辑正确性
2. 哈希表使用误区
错误示例:
// 错误:先插入再查找,可能导致使用相同元素两次
std::vector<int> twoSum(std::vector<int>& nums, int target) {
std::unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {
map[nums[i]] = i; // 先插入
int complement = target - nums[i];
if (map.count(complement)) { // 后查找
return {map[complement], i};
}
}
return {};
}
调试过程: 使用测试用例 [3,3],target=6 时,会返回 [0,0],因为元素3在查找时找到了自己。
正确做法:
// 正确:先查找再插入
std::vector<int> twoSum(std::vector<int>& nums, int target) {
std::unordered_map<int, int> map;
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
if (map.count(complement)) { // 先查找
return {map[complement], i};
}
map[nums[i]] = i; // 后插入
}
return {};
}
避免方法:
- 理解哈希表查找和插入的逻辑关系
- 通过小例子验证操作顺序
3. 递归深度风险
潜在问题: 对于大规模数据(如题目提示中的10⁴上限),递归解法可能导致栈溢出。
调试过程: 在处理大规模数据时,程序出现栈溢出错误或异常终止。
避免方法:
- 评估递归深度与输入规模的关系
- 对于大规模数据,优先考虑迭代解法
- 在生产环境中谨慎使用递归处理大规模数据
4. 索引重复使用错误
错误示例:
// 错误:未检查找到的元素是否是当前元素本身
std::vector<int> twoSum(std::vector<int>& nums, int target) {
std::unordered_map<int, int> map;
// 第一次遍历:构建哈希表
for (int i = 0; i < nums.size(); i++) {
map[nums[i]] = i;
}
// 第二次遍历:查找目标值
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
if (map.count(complement)) { // 未检查索引是否相同
return {i, map[complement]};
}
}
return {};
}
调试过程: 使用测试用例 [3,2,4],target=6 时,可能会错误地返回 [0,0]。
正确做法:
// 正确:检查找到的元素不是当前元素本身
std::vector<int> twoSum(std::vector<int>& nums, int target) {
std::unordered_map<int, int> map;
// 第一次遍历:构建哈希表
for (int i = 0; i < nums.size(); i++) {
map[nums[i]] = i;
}
// 第二次遍历:查找目标值
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
if (map.count(complement) && map[complement] != i) { // 检查索引是否不同
return {i, map[complement]};
}
}
return {};
}
避免方法:
- 仔细阅读题目要求
- 使用多种测试用例验证代码正确性
实际性能考量
- 对于小数据集(n < 100),暴力解法可能因为没有哈希开销而更快
- 对于大数据集,哈希表解法优势明显
- STL 的 unordered_map 虽然平均复杂度是 O(1),但常数因子较大
- 在生产环境中,可以考虑根据数据规模选择不同算法的混合策略