LeetCode 1 两数之和

LeetCode 1 两数之和

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


题目

🟢 两数之和

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

text
复制代码
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 跑一下:

text
复制代码
开始,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) 空间。这题要下标,排序就没优势了。


0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
Nore
下载 APP