LeetCode 1 两数之和

LeetCode 1 两数之和

LeetCode 1 两数之和,哈希表入门题,顺便记几个容易踩的坑。


题目

🟢 两数之和

数组里找两个数加起来等于 target,返回下标。只有唯一解,同一个元素不能用两次。

复制代码
nums = [3, 2, 4], target = 6  →  [1, 2]
nums = [2, 7, 11, 15], target = 9  →  [0, 1]

暴力做法

第一反应肯定是两层循环:

java 复制代码
for (int i = 0; i < n; i++) {
    for (int j = i + 1; j < n; j++) {
        if (nums[i] + nums[j] == target) {
            return new int[]{i, j};
        }
    }
}

这玩意 LeetCode 上能过,测试数据没卡那么死。但实际上 O(n²),n 到 10⁴ 就是 5000 万次比较,换个语言或者数据再大一点就炸了。

慢在哪?内层循环就是一句话:对于当前数,去后面找 target - 当前数。这个"找"是 O(n) 的------每次都要把剩下的扫一遍。


换 HashMap

换个问法:遍历的时候,每看到一个数 x,就问一句------"target - x 之前出现过吗?"

用 HashMap 把之前见过的数都记下来(值 → 下标),查一次 O(1)。边扫边记边查,一趟完事。

nums = [3, 2, 4], target = 6 跑一下:

复制代码
开始,map 空的

i=0,值是 3
  需要 6-3=3,map 里没有 → 把自己存进去 {3:0}

i=1,值是 2
  需要 6-2=4,map={3:0},没有 4 → 存进去 {3:0, 2:1}

i=2,值是 4
  需要 6-4=2,map 里有!下标是 1 → 返回 [1, 2]

代码:

java 复制代码
public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        int need = target - nums[i];
        if (map.containsKey(need)) {
            return new int[]{map.get(need), i};
        }
        map.put(nums[i], i);
    }
    return new int[]{-1, -1}; // 题目说了一定有解,走不到这
}

时间 O(n),空间 O(n)(运气不好要存到最后两个才找到)。


踩过的坑

先存后查,自己配自己

最开始我是这么写的:

java 复制代码
map.put(nums[i], i);                         // 先存
if (map.containsKey(target - nums[i])) { ... } // 再查

nums = [3, 2, 4], target = 6 跑过了,[3, 3], target = 6 也跑过了,以为没问题。后来碰到一个 case 才发现------i=0 时 target - 3 = 3,刚把自己存进去马上查,map 里当然有 3,直接返回 [0, 0]

就是顺序问题,先查后存就行。先看历史记录里有没有我要的,没有的话再把自己写进历史,留给后面的人用。

重复元素把下标覆盖了

nums = [3, 3], target = 6。第二个 3 来的时候,如果先存后查,put 会覆盖掉第一个 3 的下标,map 从 {3:0} 变成 {3:1}。好在题目保证只有唯一解,但下标变了总归不舒服。

先查后存刚好绕开------第二个 3 来的时候,查的是覆盖之前的 map(里面还是 {3:0}),直接返回 [0, 1],根本不会走到 put 那一步。

containsKey 写成了 contains

java 复制代码
if (map.contains(need)) { ... }  // 错!

containsArrayList 的,HashMap 里也有但它是查 value 的(O(n)),不是查 key。LeetCode 不会报错,但语义完全不对,运气不好还会超时。

最后那行 return

java 复制代码
return new int[]{-1, -1};

Java 编译器不管你逻辑上能不能走到这,方法签名的每一条分支都必须有 return。不写直接编译报错 missing return statement。写个 {-1, -1} 兜底就行,反正题目保证有解。

值范围已知时可以用数组

Map<Integer, Integer> 涉及 int 和 Integer 之间的装箱拆箱,n 不大的时候无所谓,但面试官如果问"还能更快吗"------如果题目给了值范围(比如 1~1000),直接用 int[] 替代 HashMap:

java 复制代码
int[] index = new int[1001];
Arrays.fill(index, -1);
for (int i = 0; i < nums.length; i++) {
    int need = target - nums[i];
    if (need >= 0 && need <= 1000 && index[need] != -1) {
        return new int[]{index[need], i};
    }
    index[nums[i]] = i;
}

省了 hash 计算和自动装箱,常数会小很多。但这题没给范围,老老实实用 HashMap。


复杂度

暴力:比较次数 = (n-1) + (n-2) + ... + 1 = n(n-1)/2 → O(n²),空间 O(1)。

HashMap:遍历 n 次,每次 containsKey + put 均摊 O(1) → O(n),空间 O(n)。

严格来说 Java 的 HashMap 极端情况下(所有 key 哈希碰撞)会退化成链表,单次操作 O(n)。但 Integer 的 hashCode 就是它自己,除非故意构造,正常数据不会撞。说"期望 O(n)"就行。


为什么不排序 + 双指针

LeetCode 167(两数之和 II)是排好序的数组,双指针 O(n) + O(1) 空间,很漂亮。

但这题没排序。如果先排序再双指针:排序 O(n log n),还得额外记原始下标(pair 数组存值和索引),总开销 O(n log n) 时间 + O(n) 空间,比 HashMap 慢还啰嗦。

只有一种情况排序双指针更优------题目不要求返回下标,只返回值,并且要求 O(1) 空间。这题要下标,排序就没优势了。


相关推荐
今天AI了吗4 小时前
精细化落地教程:TimechoAI调参标准+数据清洗规范+误差归因+阈值适配全维度指南
大数据·人工智能·算法
zephyr055 小时前
从递归到迭代:二叉树非递归前中后序遍历详解
算法
evans在进步5 小时前
LeetCode 2 两数相加:链表模拟加法,Java 图解进位过程
java·leetcode·链表
Dr.kangder5 小时前
嵌入式总线设备解析——TTE总线应用与实践
开发语言·网络·算法·嵌入式·多任务·同步机制
Hi李耶5 小时前
【LeetCode】557.反转字符串中的单词 III
算法·leetcode·职场和发展
深蓝学院5 小时前
机器人学习算法五大体系详解:模仿、强化、多模态、持续学习……
算法·机器人
2401_858286115 小时前
OS82.【Linux】设计线程池
java·linux·运维·服务器·开发语言·算法·线程池
Epiphany.5566 小时前
找到所有好字符串(记忆化搜索+KMP)(里面含KMP模板,用来入门)
算法
坚持编程的菜鸟6 小时前
模拟实现strncpy
c语言·算法·模拟实现strncpy