LeetCode算法题详解 1:两数之和

目录

  • 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 思路一:暴力枚举法(最直观)

核心思想

遍历所有可能的元素组合,检查它们的和是否等于目标值。

算法步骤

  1. 使用两层循环,外层循环遍历每个元素作为第一个加数
  2. 内层循环遍历外层循环之后的元素作为第二个加数
  3. 检查两数之和是否等于目标值
  4. 找到后立即返回结果

时间复杂度分析

  • 最坏情况: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]。如果这个补数已经出现过,我们就找到了答案。

关键点

  1. 边遍历边存储:在遍历过程中将元素存入哈希表
  2. 查找补数:对于当前元素,查找它的补数是否已在哈希表中
  3. 避免重复使用:先查找再存储,确保不会重复使用同一元素

算法步骤

  1. 创建HashMap,键为元素值,值为元素索引
  2. 遍历数组中的每个元素 nums[i]
  3. 计算补数 complement = target - nums[i]
  4. 检查补数是否在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);
        }
    }
}

为什么这种方法是正确的?

关键在于处理顺序:对于每个元素,我们先检查它的补数是否已经出现过,然后再将自己加入哈希表。这样确保了:

  1. 不会重复使用同一个元素
  2. 不会漏掉任何可能的组合
  3. 只需要遍历一次数组

性能优化技巧

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 思路三:排序+双指针法(特殊情况)

适用场景

这种方法不直接适用于本题,因为排序会改变元素的原始索引。但如果问题变形为只需要返回数值,或者允许额外空间存储索引信息,这是一种高效的解法。

核心思想

  1. 将数组排序
  2. 使用两个指针从两端向中间移动
  3. 根据两数之和与目标值的比较,决定移动哪个指针

算法步骤

  1. 创建数组副本并排序,同时记录原始索引
  2. 初始化两个指针:left = 0, right = n-1
  3. 当 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 选择策略建议

  1. 面试场景:首选哈希表解法,展现对数据结构和时间复杂度的理解
  2. 竞赛场景:根据数据规模选择,通常哈希表最稳妥
  3. 生产环境
    • 如果内存充足,选择哈希表
    • 如果内存紧张但数据有序,考虑双指针
    • 如果数据量很小,暴力法可能更简单

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.总结

两数之和问题虽然简单,但它像一面镜子,反映了一个程序员的算法思维和编码能力。通过这道题,我们学习到了:

  1. 暴力解法:最简单直接,适用于小规模数据
  2. 哈希表解法:空间换时间的典范,是最优解
  3. 双指针解法:适用于有序数据的特殊情况

更重要的是,我们学会了如何分析问题、选择合适的数据结构、优化算法复杂度。这些技能不仅适用于两数之和问题,也是解决更复杂算法问题的基础。

相关推荐
YuTaoShao几秒前
【LeetCode 每日一题】1339. 分裂二叉树的最大乘积
算法·leetcode·职场和发展
Neil今天也要学习几秒前
永磁同步电机控制算法--基于增量式模型的鲁棒无差拍电流预测控制
单片机·嵌入式硬件·算法
leoufung2 分钟前
LeetCode 172. Factorial Trailing Zeroes 题解
算法·leetcode·职场和发展
姓蔡小朋友5 分钟前
算法-子串
java·数据结构·算法
梭七y15 分钟前
【力扣hot100题】(131)排序链表
算法·leetcode·链表
副露のmagic18 分钟前
更弱智的算法学习 day18
学习·算法
byzh_rc19 分钟前
[数字信号处理-入门] 采样定理
算法·matlab·信号处理
想进个大厂20 分钟前
代码随想录day6哈希表
算法·leetcode·散列表
圣保罗的大教堂30 分钟前
leetcode 1339. 分裂二叉树的最大乘积 中等
leetcode
less is more_093034 分钟前
文献学习——计及分时电价的电缆配电网多时段二阶段有功与无功协调快速鲁棒优化调度方法
笔记·学习·算法