JavaLeetCode 第一题「两数之和」:从暴力枚举到一遍哈希表的正确与错误实现,详解HashMap核心知识点及常见陷阱

一、引言

LeetCode 第一题「两数之和」是无数程序员入门算法的起点。这道题解法多样,从直观的双重循环到优雅的哈希表,背后涉及了 HashMap 的使用、自动装箱拆箱、重复元素处理等诸多 Java 基础与进阶知识。本文将用四段代码,从暴力枚举到两遍哈希表,再到一遍哈希表的正确版与一个极易出错的版本,全面梳理这些知识点,帮助读者真正理解"为什么这样写是对的,那样写会翻车"。


二、解法1:暴力枚举

最直接的思路:用两层循环遍历所有数对,检查它们的和是否等于 target

复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 外层循环:固定第一个数,遍历到倒数第二个即可
        for (int i = 0; i < nums.length - 1; i++) {
            // 内层循环:第二个数从 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];
    }
}

知识点汇总表格

知识点 说明
双层 for 循环 穷举所有数对,时间复杂度 O(n²)
避免重复组合 内层 j = i + 1,保证不会把同一元素用两次,也不重复检查 (i,j) 和 (j,i)
返回数组 new int[]{i, j} 直接创建并初始化匿名数组返回
空数组返回 new int[0] 在没有解时返回长度为 0 的数组,避免返回 null
时间复杂度 O(n²),空间复杂度 O(1)

三、解法2:两遍哈希表

先遍历一次将每个数的值和它的索引存入 HashMap,再遍历一次查找补数。利用哈希表将查找时间降为 O(1),整体 O(n)。

复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 创建 HashMap,键为数组元素值,值为对应索引
        Map<Integer, Integer> map = new HashMap<>();
        
        // 第一遍:将所有元素及其索引存入 map
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i], i);  // 如果出现重复元素,后面的索引会覆盖前面的
        }
        
        // 第二遍:遍历数组,寻找补数
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            // 检查补数是否存在,并且补数的索引不是当前元素自己
            if (map.get(complement) != null && map.get(complement) != i) {
                return new int[]{i, map.get(complement)};
            }
        }
        return new int[0];
    }
}

为什么要判断 map.get(k) != i

假设 nums = [3, 2, 4]target = 6。第一遍存完之后 map 包含 3→0, 2→1, 4→2。当 i = 0 时,补数是 3map.get(3) 得到 0,此时 0 == i,如果不排除就会返回 [0,0],实际上元素 3 被使用了两次。正确答案是 [1,2]。因此 必须确保补数的索引不是当前索引

知识点汇总表格

知识点 说明
Map<Integer, Integer> 泛型接口,存储键值对;常用实现 HashMap
put(K, V) 将键值对插入映射;若键已存在则更新值
get(K) 返回键对应的值,若键不存在返回 null
!= null 判断 避免对 null 进行拆箱操作导致 NullPointerException
自动拆箱与 != i map.get(k) 返回 Integer,与 int i 比较时自动拆箱为 int,比较的是值
防止同一元素用两次 通过 map.get(complement) != i 检查索引不同
时间复杂度 O(n),空间复杂度 O(n)

四、解法3:一遍哈希表(正确版)

在两遍哈希表的基础上,将"查找补数"和"存入当前数"合并到一个循环中。关键:先查找,后存入

复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        // 哈希表:键为元素值,值为索引
        Map<Integer, Integer> hashTable = new HashMap<>();
        
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            // 先检查补数是否已经在表中
            if (hashTable.containsKey(complement)) {
                // 存在,直接返回结果(补数索引必不为 i,因为当前元素还没被放入)
                return new int[]{hashTable.get(complement), i};
            }
            // 后存入当前元素,保证不会匹配到自己
            hashTable.put(nums[i], i);
        }
        return new int[0];
    }
}

顺序的魔力 :因为当前元素在检查补数时还未进入哈希表,所以查找到的补数一定是之前遍历过的其他元素。这天然避免了"同一个元素用两次"的问题,也无需再写 != i


五、解法4:一遍哈希表(错误版)及原因分析

如果将 putcontainsKey 的顺序写反,就会踩坑。

复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> hashTable = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            // 错误:先把当前元素放入哈希表
            hashTable.put(nums[i], i);
            // 再检查补数 ------ 这时补数可能就是刚放进去的自己!
            if (hashTable.containsKey(target - nums[i])) {
                return new int[]{hashTable.get(target - nums[i]), i};
            }
        }
        return new int[0];
    }
}

报错场景还原

nums = [3, 2, 4], target = 6 为例:

循环轮次 i nums[i] 操作 哈希表状态 containsKey 检查 返回结果
第一轮 0 3 put(3,0) {3=0} containsKey(3) → true [0, 0]

i=0 时,3 刚被放入哈希表,补数 target - 3 = 3 立即在表中被找到,hashTable.get(3) 返回 0,于是输出 [0, 0],错误地把一个元素用了两次。正确答案 [1, 2] 被完全错过。

即使数组是 [3, 3]target = 6,错误版在 i=0 时也会返回 [0, 0],而不是期待的 [0, 1]。因为第二个 3 还没遍历到,此时表中只有一个 3,匹配的还是自身。

教训 :使用一遍哈希表时,必须先检查,后放入

正确版与错误版对比表格

对比维度 正确版(先查后放) 错误版(先放后查)
放入时机 检查补数之后 检查补数之前
是否会匹配自身 不会(自身未入表) 会(自身刚入表)
是否需要额外 != i 判断 不需要 仍会出现同一元素用两次的问题
典型错误输出 [0, 0]
正确性 ✅ 正确 ❌ 错误

六、拓展知识点全景

1. HashMap 核心方法详解

方法 描述 注意事项
put(K key, V value) 插入键值对;若键已存在,新值覆盖旧值,返回旧值 在重复元素场景下,后出现的索引会覆盖先前的
get(Object key) 返回键对应的值,不存在则返回 null 返回值可能是 null,拆箱前需判空
containsKey(Object key) 判断键是否存在 时间复杂度 O(1),常用于检查后再操作
containsValue(Object value) 判断值是否存在 时间复杂度 O(n),需遍历
remove(Object key) 删除键值对,返回被删除的值 不存在则返回 null
size() / isEmpty() 返回元素个数 / 判断是否为空
keySet() / values() / entrySet() 返回键集、值集合、键值对集 常用于遍历

2. 自动装箱与拆箱对比较的影响

Java 中 Integer 是对象,int 是基本类型。当 Integerint 进行 ==!= 比较时:

  • Integer 会自动拆箱为 int,然后进行值比较

  • 这保证了 map.get(k) != i 是在比较数值而非引用。

但如果用两个 Integer 进行 == 比较,则比较的是引用地址 。在 -128 ~ 127 范围内由于享元模式会缓存对象,相同值可能引用相同;超出范围则每次自动装箱会新建对象,== 可能得到 false所以在对象比较时务必使用 equals() 或确保一边是基本类型。

复制代码
Integer a = 200;
Integer b = 200;
System.out.println(a == b);   // false(不同对象)
System.out.println(a.equals(b)); // true

// 在 map.get() != i 中:
// map.get 返回 Integer,i 是 int,Integer 被拆箱成 int,安全

3. 哈希表的时间复杂度与冲突处理

  • 理想情况:O(1) 插入和查找。

  • 哈希冲突 :不同的键计算出相同的桶位置。HashMap 在 JDK 8 中采用 数组 + 链表 + 红黑树 的结构:

    • 当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树,查找复杂度降为 O(log n)。

    • 当树节点数 ≤ 6 时,退化回链表。

      // 简单示意 HashMap 内部结构(JDK 8+)
      // Node<K,V>[] table; // 哈希桶数组
      // Node 可能链向下一个节点形成链表
      // 当链表过长时 TreeNode 替代 Node 形成红黑树

4. Map 接口的常见实现对比

实现类 底层结构 是否有序 线程安全 适用场景
HashMap 数组+链表+红黑树 无序 通用场景,查找插入快
LinkedHashMap 哈希表+双向链表 保持插入顺序 需要顺序遍历,如 LRU 缓存
TreeMap 红黑树 键自然排序 需要键排序的场景
Hashtable 哈希表 无序 是(方法 synchronized) 遗留类,不推荐
ConcurrentHashMap 分段锁+CAS+红黑树 无序 高并发场景

5. 关于返回 new int[0] 的思考

当没有解时,返回 null 可能导致调用方出现 NullPointerException。返回空数组 new int[0] 是一种防御性编程 实践。调用方可以安全地使用 result.length 而不必判空。


七、总结

从暴力枚举到两遍哈希表,再到一遍哈希表,我们看到了算法优化的清晰路径,也看到了一个小小的顺序错误如何让程序结果完全错误。核心要点:

  1. 暴力法虽然慢,但直观可靠,是验证复杂解法的基准。

  2. 两遍哈希表 将复杂度降到 O(n),但需手动检查 != i 防止元素重用。

  3. 一遍哈希表最优雅,通过"先查后放"自然避免了重用问题;反之"先放后查"会酿成大错。

  4. 背后涉及的 HashMap 方法、自动拆箱、哈希冲突、防御性编程,都是每个 Java 开发者必须掌握的基本功。

相关推荐
黎阳之光2 小时前
视频孪生重构轨交数字孪生新范式|黎阳之光以自主核心技术破解落地难题
大数据·人工智能·算法·安全·数字孪生
JackSparrow4142 小时前
彻底理解Java NIO(一)C语言实现 单进程+多进程+多线程 阻塞式I/O 服务器详解
java·linux·c语言·网络·后端·tcp/ip·nio
小江的记录本2 小时前
【微服务与云原生架构】Serverless架构、FaaS/BaaS、核心原理、优缺点
java·后端·微服务·云原生·架构·系统架构·serverless
谢谢 啊sir2 小时前
L2-060 大语言模型的推理 - java
java·人工智能·语言模型
云淡风轻~窗明几净2 小时前
关于TSP的sealine算法与角谷猜想(2026-04-25)
数据结构·人工智能·算法·动态规划·模拟退火算法
wayz112 小时前
Day 13:朴素贝叶斯分类器
人工智能·算法·机器学习·朴素贝叶斯
白夜11172 小时前
C++(mixins 混入模式)
开发语言·c++·笔记
前端摸鱼匠2 小时前
【AI大模型春招面试题29】对比学习(Contrastive Learning)在大模型预训练中的应用?
人工智能·学习·算法·面试·大模型·求职招聘
探物 AI2 小时前
【感知·单目测距】单目摄像头测距原理与前向碰撞预警(FCWS)实现
算法·目标检测·计算机视觉