leetcode1 两数之和 [easy]

82 阅读7分钟

题目要求

  1. 给定一个整数nums和一个目标值target,在该数组中找出和为目标值的那两个整数并返回他们的数组下标;

  2. 可以假设输出仅对应一个答案,但是是不能重复利用数组中的元素;

    其中:

    2 <= nums.length <= 1e4

    -1e9 <= nums[i] <= 1e9

    -1e9 <= target <= 1e9

方法一:遍历法(for循环)

基本思路

两层嵌套的for循环遍历所有可能的元素组合,元素组合中的两个元素变量分别设为i和j。其中:i从0到n-1,j从i到n-1,选组组合的种类数为:Cn2=n(n1)2n2C_{n}^{2} = \frac{n(n-1)}{2}\approx n^{2}。代码通过循环逐个访问数组元素,并判断是否符合条件: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 {};
   }
};

复杂度分析

  • 时间复杂度O(N2)O(N^2)NN是数组中的元素数量,最坏情况下数组中的任意两个不同下标的数都需要匹配一次。
  • 空间复杂度O(1)O(1)

几点注释

  1. C++标准序的vector容器:

    代码使用vector<int>作为参数和返回值,涉及:vector的声明,元素访问nums[i],初始化return{i , j}和空间容器返回return{}

    如:直接通过{i , j}构造并返回vector是C++11的列表初始化特征。

  2. 函数返回值与类成员函数:

    函数定义在Solution类中,是面向对象编程的体现。返回类型为vector<int>,符合题目要求的格式。若未找到解,返回空vector{}

  3. C++语法特征:

    列表初始化:return {i, j}利用了C++11的统一初始化语法,简化代码。

    作用域限定:循环变量ij的作用域限制在循环内部,符合现代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 {};
   }
};

复杂度分析

  • 时间复杂度:O(NlogN)=O(Nlogn)+O(N)O(NlogN)=O(Nlogn)+O(N),其中:O(NlogN)O(NlogN)为排序时间复杂度;O(N)O(N)为双指针遍历时间复杂度。优于暴力解法的 O(N2)O(N^2)
  • 空间复杂度O(1)O(1)

几点注释

1.索引保存:排序会打乱原始索引,因此需要将值和索引一起存储(std::pair<int, int>)。

2.排序操作:使用std::sort对数组进行排序,排序的时间复杂度为 O(NlogN)O(NlogN)

3. 双指针逼近:

左指针 left 从数组起始位置开始,右指针 right 从数组末尾开始。

根据当前两数之和与目标值的比较,决定移动左指针(和太小)或右指针(和太大)。

4. 双指针法更适用于类似“三数之和”等扩展问题。

方法三:哈希表解法(边遍历边查询)

基本思路

哈希算法是一种通过哈希函数将数据映射到固定大小的表中,以实现快速查找和插入的技术。其核心思想是用空间换时间,平均情况下可实现O(1)O(1)的时间复杂度。在解决两数之和问题时,哈希表被用来高效地存储和查询目标值与当前元素的差值。

首先对哈希表初始化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 {};  
    }
};

复杂度分析

  • 时间复杂度O(N)O(N):每个元素仅遍历一次,哈希表的插入和查找操作平均为O(1)O(1);
  • 空间复杂度O(N)O(N):最坏情况下需要存储所有元素。

几点注释

避免重复元素的处理方法:

由于哈希表在插入前先进行查询,即使数组中有重复元素,也能正确处理。例如,nums = [2, 2]target = 4时,第二个2的补数(2)已存在于哈希表中,直接返回正确索引。

哈希算法的优势:

通过哈希表将查找补数的时间复杂度优化至O(1)O(1),实现了高效的线性时间复杂度。其核心在于边遍历边查询,既避免了重复元素的干扰,又减少了冗余计算,是典型空间换时间的策略

总结

方法时间复杂度空间复杂度保留索引处理重复元素适用场景
哈希法O(N)O(N)O(N)O(N)天然支持数据无序且需要保留原始索引,对时间复杂度敏感,空间充足。
遍历法O(N2)O(N²)O(1)O(1)支持极小数据规模或验证正确性
双指针法O(NlogN)O(N log N)O(N)O(N)O(1)O(1)需额外操作需手动跳过数据可排序且对空间敏感

  1. 是否需要保留原始索引

    • 哈希法、暴力法直接支持,双指针法需额外存储。
  2. 数据规模

    • 大规模数据选哈希法,极小数据选暴力法,中等规模可考虑双指针。
  3. 数据是否有序

    • 若已有序,双指针法更高效。
  4. 时间 vs. 空间权衡

    • 时间优先选哈希法,空间优先选双指针法。

综上:

  • 哈希法是解决两数之和的最优选择,兼顾时间效率和代码简洁性。
  • 双指针法在空间敏感或数据有序时表现优异,但需牺牲索引或增加预处理成本。