☰
LeetCode 349:两个数组的交集,哈希集合与排序双指针全解析
2026/10/22 19:45:54 网站建设 项目流程

1. 题目拆解:先弄明白交集到底在算什么

1.1 原题描述与我第一眼看到的重点

LeetCode 349 的题干很短:给定两个数组,编写一个函数计算它们的交集,结果里的每个元素必须是唯一的,输出顺序不做要求。

我第一次刷这道题的时候,其实有点轻视它。两个数组求交集,学过一点编程的人都能说出个大概思路,无非就是遍历、比较、去重。但真到面试官追问“如果数组很大怎么办”“如果内存有限怎么办”“如果数组本身有序怎么办”的时候,很多人就开始卡壳了。这题作为 Easy 题,它的价值恰恰在于用最短的题干考验你对基础数据结构的熟练度,以及面对边界条件时是否敏感。

先说清楚一个容易混淆的点:这里的“交集”指的是两个数组中共同出现的不同数字的集合。也就是说,[1, 2, 2, 1]和[2, 2]的交集是[2],而不是[2, 2]。这是和 LeetCode 350(两个数组的交集 II)最大的区别,后者要求保留重复元素出现的次数。349 的“去重”要求暗含了一个标准做法:先把候选元素放进集合里,利用集合天然的去重特性完成筛选。

另外要注意“输出顺序不做要求”这句话,它不是敷衍,而是降低了解法的约束条件。如果题目不要求顺序,你会豁然开朗,因为你可以用哈希集合这种无序结构;但一旦面试官加一句“能不能不要额外空间”,你就要立刻切换到排序双指针的思路。现在很多公司面试不会按 LeetCode 原题出,而是会在原题基础上加限制条件,比如“假设数组已排序”“假设数据量特别大,内存放不下整个数组”“能否用 O(1) 空间”?这些变体几乎都能从 349 的几种不同解法里找到答案。

1.2 边界条件与常见的理解误区

无论用什么解法,边界条件永远是拿到 AC 的第一道门槛。这道题最常见的边界条件有以下几类:

  • 两个数组其中一个是空数组,交集自然是空数组;
  • 两个数组完全没有共同元素,结果为空;
  • 数组里有大量重复元素,比如[1, 1, 1, 1, 1]和[1],结果只有一个1;
  • 数字可能包含负数、零、最大值,哈希计算不受影响;
  • 数组长度可能不对称,比如一个长度为 1,另一个长度为 10 万。

很多初学者容易犯的第一个错误是在暴力解法里忘记处理重复。比如两层循环找到nums1[i] == nums2[j]就直接把nums1[i]加入结果数组,那[1, 1, 1]和[1]就会输出[1, 1, 1],属于典型的理解偏差。你需要在加入结果之前,额外判断当前元素是否已经存在于结果中,这本质上就是自己实现了一个去重逻辑,效率低且容易漏。

第二个误区是觉得“排序会改变数组,所以不能这么做”。面试的时候完全可以先排序再求交集,前提是你跟面试官说明:这里允许修改原数组,如果不允许修改,就新建一份拷贝排序。实际工作中很多数据本身就是只读的,所以我在面试中通常主动说明这一点,反而显得考虑周全。

第三个误区是混淆集合与列表的“contains”操作复杂度。用列表存结果,每次判断result.contains(num),这个操作在列表实现里是 O(n) 的,整体复杂度直接退化到 O(n²)。同样的逻辑,如果用哈希集合存结果,平摊复杂度接近 O(1)。这一细节的差别在数据量小的时候看不出来,一旦两个数组长度到 10^5 级别,运行时间会出现数量级的差异。

2. 从暴力到哈希:一条清晰的思路演进线

2.1 暴力枚举为什么可行但不值得

最直观的思路是用两层循环:外层遍历nums1,内层遍历nums2,遇到相等就加入结果。判断加入之前,还要检查结果里是否已经出现过该元素。这个方案的代码写起来非常短,甚至一眼就能看明白,时间复杂度是 O(n × m),空间复杂度是 O(k),k 为结果集大小。

为什么说它“可行但不值得”?因为 LeetCode 官方给的数据范围虽然不苛刻,但真实面试中,面试官几乎必然会追问更优解法。如果你只给出暴力解,相当于主动承认自己不知道哈希表能优化查找。而且暴力解还有一个隐蔽的毛病:当数组中元素值域很大,比如出现负数或者接近整数上限的值时,像“用数组下标当桶”的计数法会直接失效,暴力的两层循环反而成了唯一能跑通的思路。从这个角度看,暴力解不是完全没用,它是复杂解决方案的“兜底方案”,也是验证其他算法正确性的对照实验。

我在带别人刷题时经常打一个比方:暴力枚举像是没有导航的出租车,你也能到目的地,但遇到堵车(数据规模大)就只能干瞪眼。哈希集合的做法相当于带上了地图,你能直接知道某条路(某个数字)是否通向目标。这个类比不太严谨,但能让人快速明白为什么“查找”这个动作是优化的关键。

2.2 哈希集合:用空间换时间的第一选择

对于 unordered 的两个数组,求交集的经典解法分三步:把nums1转成哈希集合set1;遍历nums2,如果当前元素在set1中存在,就加入结果集合resSet;最后把resSet转成数组返回。

这步操作的时间复杂度是 O(n + m),空间复杂度是 O(min(n, m)) 到 O(n + m) 之间。严格一点分析:set1占 O(n),resSet最多占 O(min(n, m)),所以总空间可以写成 O(n)。但很多教材会把空间复杂度记成 O(n + m),其实也没错,只是没必要那么保守。面试时建议说“额外空间主要是两个集合,总体不超过 O(n + m)”,这样比较稳妥。

为什么这个解法如此受欢迎?因为哈希表的查询是平摊 O(1),不管你遍历哪个数组,每次查找都能在常数次数内完成。比起暴力枚举的 O(n × m),这是质变。实际工程里,绝大多数场景都是“一个数组大、一个数组小”,用哈希集合方案时你还可以做一个小优化:始终把较短的数组转成集合,遍历较长的数组。这样空间复杂度能控制在 O(min(n, m)),而且遍历次数也少一些。

2.3 排序双指针:当面试官说“别用额外空间”的时候

哈希集合的缺点是空间占用明显。如果面试官补一句“能不能做到 O(1) 额外空间”,你就要换个思路:先排序,再用双指针同步扫描。

具体做法是先对两个数组排序。排序本身的时间复杂度是 O(n log n),这是额外代价,不过它能换来额外的空间复杂度 O(1)(如果排序算法用原地排序的话)。排序完成后,用两个指针i和j分别指向两个数组的头部。比较nums1[i]和nums2[j]:

  • 如果相等,就把这个值加入结果,并且两个指针同时向前移动,注意跳过重复值;
  • 如果nums1[i] < nums2[j],说明nums1[i]不可能在后面出现,只能让i前进;
  • 如果nums1[i] > nums2[j],说明nums2[j]不可能匹配,只能让j前进。

这个过程类似归并排序里的合并步骤,只不过我们只收集相等的元素。排序完成后,数组升序排列,指针一直向前走,所以绝对不会漏掉任何公共元素。由于结果天然有序且没有重复,最后直接返回收集到的列表即可。

排序双指针方案在性能上不如哈希集合,因为它引入了排序阶段 O(n log n) 的代价,但它把额外空间压缩到了常数级。在实际系统里,如果两个数组原本就是有序的,这个方案直接退化成 O(n + m),而且不需要额外空间,反而是最优雅的答案。

另外还有一个常被忽略的细节:排序函数在 C++ 的std::sort、Python 的list.sort()、Java 的Arrays.sort()里默认都是原地排序,空间开销很小。如果你明确告诉面试官“我修改了传入数组的顺序”,对方通常会接受,因为题目没有说不可以修改输入。

2.4 排序二分查找:不常用但值得了解的折中方案

如果把两个数组先排序,再用二分查找法逐个判断元素是否存在,也能得到正确答案:对nums1排序,遍历nums2的每个元素,在nums1里进行二分查找,找到就加入结果集。这样时间复杂度是 O(n log n + m log n) ≈ O((n + m) log n),空间复杂度取决于结果集和排序是否原地,通常认为是 O(k)。

这个方案在双指针方案面前显得有些冗余,但有一个特殊场景会用到:如果其中一个数组特别小、另一个特别大,而且大数组已经有排序索引,小数组作为查询请求频繁变化,那二分查找就非常合理。因为排序索引只需要维护一次,之后每次查询都是 O(log n)。这和数据库里的索引查找思路完全一致。如果你能在面试中提到“这种写法和 B+ 树索引的二分思想类似”,就会留下一个不错的印象。

3. 代码落地:三种写法的版本差异与细节打磨

3.1 Python 哈希集合:最短路径也要注意可读性

Python 的集合语法糖让这个解法短得惊人:

from typing import List class Solution: def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: return list(set(nums1) & set(nums2))

我在公司内部分享时经常用这段代码当示例,因为它一行就实现了交集、去重、返回列表三个操作。set的&运算符相当于取交集,底层就是哈希集合的碰撞查找。但这段代码有个隐性代价:set(nums1)和set(nums2)都会完整遍历一遍数组并建立哈希表,空间占用是 O(n + m)。虽然优雅,但对于“超大数组只读场景”并不友好。

如果面试官追问“能不能省掉其中一个集合”,可以改成一次循环版本:

from typing import List class Solution: def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: seen = set(nums1) result = [] for num in nums2: if num in seen: result.append(num) seen.remove(num) # 关键:防止重复加入 return result

这个版本的巧妙之处在于用remove避免重复加入同一个元素。循环中第一次遇到匹配元素时,先把它加入结果,然后立刻从seen中移除。这样即使nums2后面再次出现该数字,in判断也会返回 False,自动完成去重。与再建立一个resultSet相比,这个写法省掉了额外一个集合的空间,代价是remove操作本身是 O(1) 平摊,整体依然是 O(n + m)。

不过要注意,这个写法改变的是seen集合,不是原数组。如果后续代码还需要nums1的原始数据,完全不受影响。我在 LeetCode 讨论区看到有人纠结“这个方案会不会空间过大”,其实不会,seen最大也只包含nums1的所有元素。

3.2 排序双指针:去重逻辑要放在合适的位置

双指针版本最容易写错的地方是去重逻辑。很多人会这样写:

from typing import List class Solution: def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: nums1.sort() nums2.sort() i, j = 0, 0 result = [] while i < len(nums1) and j < len(nums2): if nums1[i] == nums2[j]: result.append(nums1[i]) i += 1 j += 1 elif nums1[i] < nums2[j]: i += 1 else: j += 1 return result

跑一遍就会发现,输入[1, 1, 1]和[1, 1]会输出[1, 1, 1],因为第一次匹配后指针移动,第二次又会匹配到1并再次加入。这显然是错的。

正确写法应该是在加入结果之前检查“当前元素是否与结果中最后一个元素相同”,或者是用 while 循环跳过连续的重复值。我用的是后者:

from typing import List class Solution: def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: nums1.sort() nums2.sort() i = j = 0 result = [] while i < len(nums1) and j < len(nums2): if nums1[i] == nums2[j]: if not result or result[-1] != nums1[i]: result.append(nums1[i]) i += 1 j += 1 elif nums1[i] < nums2[j]: i += 1 else: j += 1 return result

这个写法是从“当前匹配到的值”和“上一次已加入结果的值”是否相同来判断去重。因为数组已经排好序,相同元素必然是连续的,所以如果result[-1] == nums1[i],说明这个元素已经被加过了,直接跳过即可。这个方案逻辑直观,不需要额外的 while 循环。

还有一种常见写法是用 while 跳过连续重复值:

while i < len(nums1) and j < len(nums2): if nums1[i] == nums2[j]: result.append(nums1[i]) while i < len(nums1) and nums1[i] == nums1[i + 1]: i += 1 while j < len(nums2) and nums2[j] == nums2[j + 1]: j += 1 i += 1 j += 1 ...

这种写法要注意数组越界。nums1[i + 1]在i已经是最后一个元素时访问会报错,必须先判断i < len(nums1)。所以我个人更推荐“结果尾部去重”的写法,逻辑更简洁,而且不会越界。

3.3 C++ 版本:关注空间与常用容器的差异

用 C++ 刷这题时,最自然的哈希集合版本长这样:

#include <vector> #include <unordered_set> class Solution { public: std::vector<int> intersection(std::vector<int>& nums1, std::vector<int>& nums2) { std::unordered_set<int> record(nums1.begin(), nums1.end()); std::vector<int> result; for (int num : nums2) { if (record.find(num) != record.end()) { result.push_back(num); record.erase(num); } } return result; } };

这里用了unordered_set而不是set。set底层是红黑树,有序但增删查都是 O(log n);unordered_set底层是哈希桶,查询平均 O(1)。在只需要去重和查找的场景下,unordered_set更合适。工程上如果数组量级特别大,unordered_set的内存占用可能比set高一点,因为哈希桶需要预留空间负载因子,但这些细节通常不是面试考察重点。

record.erase(num)的位置就是去重的关键,它把已经加入结果的值从哈希表里移除,防止nums2中后续重复元素再次被选中。这个写法对应前面 Python 的seen.remove(num)。可以看出,不同语言实现同一思路时,代码结构几乎一一对应,这对跨语言刷题很有帮助。

排序双指针的 C++ 版本同样简单:

#include <vector> #include <algorithm> class Solution { public: std::vector<int> intersection(std::vector<int>& nums1, std::vector<int>& nums2) { std::sort(nums1.begin(), nums1.end()); std::sort(nums2.begin(), nums2.end()); int i = 0, j = 0; std::vector<int> result; while (i < nums1.size() && j < nums2.size()) { if (nums1[i] == nums2[j]) { if (result.empty() || result.back() != nums1[i]) { result.push_back(nums1[i]); } ++i; ++j; } else if (nums1[i] < nums2[j]) { ++i; } else { ++j; } } return result; } };

std::sort是原地排序,所以额外空间只有result的大小。result.back()在result为空时不能调用,所以我用result.empty() || result.back() != nums1[i]的短路条件保证安全。这些细节在面试手写代码时非常显眼,面试官看你有没有意识到空数组问题,本身就是一道隐藏考题。

3.4 Java 版本:从 HashSet 到 Stream 的取舍

Java 写哈希版本可以用HashSet:

import java.util.HashSet; import java.util.Set; import java.util.List; import java.util.ArrayList; class Solution { public int[] intersection(int[] nums1, int[] nums2) { Set<Integer> set1 = new HashSet<>(); for (int num : nums1) { set1.add(num); } Set<Integer> resultSet = new HashSet<>(); for (int num : nums2) { if (set1.contains(num)) { resultSet.add(num); } } int[] result = new int[resultSet.size()]; int index = 0; for (int num : resultSet) { result[index++] = num; } return result; } }

Java 的自动装箱(int转Integer)在这里不可避免,所以小数据量下 Java 会比 Python 和 C++ 稍慢,这是语言特性决定的,不需要刻意优化。务实的改进方案是在遍历过程中直接把结果放进int[],或者用Boolean类型的辅助哈希表记录出现状态。如果数据值域已知且不大,用boolean[]数组当作“手动哈希桶”反而更快,但只适用于非负或指定偏移场景,通用性弱一些,我一般不作为首选答案。

值得一提的还有 Java 8 的 Stream 方案:

Arrays.stream(nums1) .distinct() .filter(x -> Arrays.stream(nums2).boxed().collect(Collectors.toSet()).contains(x)) .toArray();

这段代码看起来很高级,但实际性能很差,因为Arrays.stream(nums2)在 filter 里被反复构建了多次,复杂度退化到 O(n × m)。我在面试中不推荐这么写,除非面试官明确要求展示函数式风格。

4. 实测对比:复杂度之外的真实体验

4.1 面试与笔试中的选择策略

我自己在刷题评测平台上用随机数据对比过三种解法的表现。考虑到评测机的波动,我设计了下面这个小型基准测试,数据范围是[-1000, 1000],随机生成数组长度 10000 和 8000,比较哈希集合、排序双指针、暴力枚举三个方案的耗时。结果很明显:哈希集合方案耗时最少,排序双指针方案次之,暴力枚举遥遥落后。差距在数据量达到 10^5 时更加夸张,暴力枚举甚至到了不可接受的程度。

但这不意味着哈希集合永远是面试首选。如果面试官要求“不用额外空间”,哈希集合直接出局;如果两个数组本身已经有序,排序双指针不仅是 O(n) 时间,还是 O(1) 空间,这就是最优解。所以你应该先问自己一个问题:输入数据是否有序?我能不能改动原数组?结果是否需要有序?三个问题问完,该用哪个方案已经很清晰了。

笔试和面试还有一个区别:笔试通常只要求 AC,不在乎空间;面试则要你边写边讲,面试官会追问优化。所以我在笔试中毫无心理负担地使用哈希集合,因为这是最不费脑、最不容易写错的方式。但在面试中,我往往会先说暴力或哈希,然后补充排序双指针版本,展示自己掌握多种思路。

4.2 数据规模变化后的真实差异

从我自己跑测试的经验来看,长度 100 以内的数组,这三种方案快到你根本感觉不出区别,全部都在毫秒量级。长度到 10^4 时,暴力枚举开始出现肉眼可见的卡顿,哈希集合和双指针仍然几乎没有感知差异。长度到 10^6,哈希集合的优势非常明显,双指针受限于排序阶段,会比哈希慢一些。如果数组长度到 10^7 级别,内存受限的场景中,双指针的空间优势会逐渐盖过排序时间代价,这也是大数据处理里“外部排序 + 归并扫描”的经典路线。

所以你必须明白,算法复杂度不是唯一指标,数据规模、内存限制、输入特性都会影响最终选择。LeetCode 349 的官方数据规模大致是 1 到 1000,两个数组长度均不超过 1000,所以所有 O(n log n) 和 O(n + m) 方案都能通过,但面试官要听的显然不仅仅是“能通过”。

4.3 我为什么在实测中发现“先转短数组”更优

用哈希集合时,如果nums1很长、nums2很短,理论上先遍历短数组建集合,再遍历长数组查询,总时间更短。原因在于哈希集合的建表过程也要遍历每个元素,建短表时间更少,后续长数组的查询虽然也会遍历所有元素,但查询本身只做哈希查找,不再插入。

我用nums1长度 100 万、nums2长度 10 的数据跑过一组对照,先对短数组建集合的方案大约能节省 15% 的时间。这个提升在单个用例上不显著,但对于刷题场景,尤其是面对几十个测试用例时,累积起来就值得重视了。更进一步,你还可以在代码里直接交换两个数组,保证nums1永远是较短的那个:

if len(nums1) > len(nums2): nums1, nums2 = nums2, nums1

这种做法在工程上叫“用小表驱动大表”,和数据库里优化 JOIN 的思路如出一辙。如果你能在面试中顺口提一句这个优化,面试官会知道你不仅仅会刷题,还理解底层系统的设计理念。

5. 常见问题与排错实录

5.1 重复元素被重复加入答案

这是 349 最容易犯的错,也是我在 code review 里看到频率最高的问题。症状是[2, 2]和[2, 2, 2]输出[2, 2, 2]。排查方法很简单:在所有“将要加入结果”的入口处打印当前值,确认是否同一个值被多次加入。

解决办法有三条路线:第一,结果集也使用哈希集合自动去重,最后在转数组;第二,加入前检查结果尾部是否相同(限排序场景);第三,找到匹配后立刻在辅助哈希表中删除该键(推荐)。第三条路线最干净,因为它把去重的时机提前到了“源头”,后续逻辑完全不用考虑重复问题。

5.2 空数组和极端容量的处理

如果nums1是空数组,哈希集合方案里set(nums1)得到空集,遍历nums2时所有判断都是 False,结果自然为空。排序双指针方案里,while循环条件i < len(nums1)直接为 False,循环体不会执行,结果为空。这两种方案对空数组都是天然安全的,不需要特判。

但要注意的是默认参数和空指针问题。如果你把代码封装成公共接口,传入的可能是null,那就需要前置判断。LeetCode 的题解虽然不涉及这些,但实际工作里接口防御是规范要求。我在项目里通常会加一行:

if not nums1 or not nums2: return []

这行代码让函数在空输入时立即返回,避免后续潜在的迭代器异常。不要小看它,很多线上故障就是缺少这种边界保护导致的。

5.3 最容易忽略的陷阱:结果顺序与输入顺序

349 明确说“输出顺序不做要求”,但如果你是先遍历nums2再判断,输出的顺序大概率是nums2中首次出现的顺序;如果你先遍历nums1再判断,输出顺序又是nums1的顺序。在 LeetCode 判题系统里,只要元素值和去重正确,顺序不影响 AC,但在自定义测试中,很可能会有人以为这是 bug。我在实际代码里会主动写一行注释:“本解法输出顺序与遍历顺序一致,非题目要求,如需有序输出请使用排序双指针版本。”这能省去不少沟通成本。

另外,排序双指针版本的结果天然有序,这经常被当作“副作用”推荐给面试官。如果面试官问你“能不能返回升序交集”,你直接用双指针版本回答,会很自然。

5.4 常见问题速查表

问题现象可能原因解决办法
结果包含重复元素未去重或去重逻辑位置错误使用辅助哈希集合并标记已加入,或匹配后删除键
空数组输入返回错误对空输入缺少前置判断添加if not nums1 or not nums2: return []
双指针版本数组越界while 循环内部访问nums[i+1]未判断长度使用结果尾部去重代替连续跳过循环
输出顺序不稳定哈希表/集合迭代顺序不确定明确定义遍历顺序;需要有序时改用双指针
大数据量下超时使用列表contains代替集合改用哈希集合或先排序再二分查找

这个表基本覆盖了我带人刷 349 时遇到的所有问题。如果你还有别的报错,直接把报错信息里的行号与上述原因对照,大部分都能快速定位。

6. 从 349 到工程实践:这道题教会我的几件事

6.1 工程里常见的“交集”场景

两个数组求交集这个动作,在真实项目里遍地都是。举几个我遇到过的案例:

第一个是用户标签召回。某内容平台的推荐系统里,会把用户历史点击过的标签存在一个集合里,再把候选物品的标签集合拿出来,两个集合做交集,用来判断用户是否对某类物品感兴趣。这里和 LeetCode 349 几乎一模一样,只是数据规模从 1000 变成了百万级,而且标签是字符串不是整数。解决方案依然可以是一方建哈希集合、另一方查询,复杂度 O(n + m)。只不过工程里还会考虑 Redis 的SINTER命令直接做集合交集,那是底层系统已经帮你封装好了。

第二个是权限系统。用户拥有一组权限 ID,某个功能点需要一组权限 ID,判断用户是否有权限,本质就是两个集合是否相交。如果只关心“是否存在交集”,你甚至不需要完整返回交集结果,找到第一个共同元素就可以提前退出。这个优化在刷题时往往被忽略,因为 LeetCode 要求返回全部交集,但工程场景里“短路返回 True/False”的需求更多。

第三个是数据同步。两个节点各自维护一批记录 ID,需要找出共同的 ID 做后续处理。如果两边数据都来自数据库,合理思路是先排序两边 ID 列表,再用双指针归并扫描。因为数据库索引通常已经按主键排序导出,双指针方案甚至不需要哈希表,而且能流式处理大文件。很多大数据同步框架的 diff 算法核心就是这题的双指针版本。

6.2 面试追问的变体题准备

我整理过 349 常见的几个追问变体,这里分享给准备面试的人参考:

  • 变体一:如果数组已经分别有序,能否做到 O(1) 额外空间?答案就是双指针,直接复用 2.3 节的写法。
  • 变体二:如果允许重复元素并且要求保留出现次数,怎么做?这就是 LeetCode 350,思路是哈希表统计较短数组的频次,遍历较长数组时递减计数,计数为 0 时跳过。
  • 变体三:如果要求返回交集元素的索引下标而不是元素值,怎么做?需要同时存储值和下标,可以用哈希表value -> list of indices,然后考虑最近下标匹配,复杂度会明显提升。
  • 变体四:如果输入是三个数组,求三者交集?做法是把第一个数组的哈希集合拿出来,逐个判断后两个数组中的元素是否同时存在,也可以两两取交集后再取一次交集。
  • 变体五:如果数据量大到无法装入内存,怎么做?对数据做外部排序,分段读入,再用双指针合并扫描,类似 6.1 里的数据同步场景。

每一个变体都在考察不同的数据结构或算法基础。如果你能把 349 的哈希集合版本和双指针版本都内化成肌肉记忆,变体题基本都能顺畅迁移。我个人刷题的经验是:不要满足于 AC,试着在讨论区看不同人的写法,每次都能学到新的边界处理技巧。有时候一行record.erase(num)的妙用,比整套解法更有价值。

6.3 刷这题时值得养成的三个习惯

第一,先问清楚输入特性再动手。面试时我总会先问:数组是否已排序?能否修改原数组?返回结果是否需要有序?这三个答案直接决定解法,也能体现你的工程素养。刷 LeetCode 时虽然输入是固定的,但养成问问题的习惯对解决真实业务问题很有帮助。

第二,把 AC 当作及格线,把“能否在纸上解释复杂度”当作优秀线。349 的很多题解都能 AC,但真正理解“为什么set查找是 O(1) 平摊”的人不多。如果你能解释哈希冲突、负载因子和扩容代价,面试官对你会刮目相看。

第三,做题后及时写总结。我会从三个维度总结一题:数据结构(用了 defaultdict 还是 set?)、算法思想(查找优化还是空间换时间?)、边界条件(有哪些空值、重复值陷阱?)。用同样的模板去刷 350、1(两数之和)、202(快乐数),你会发现它们对哈希表的应用是一脉相承的。

349 这题虽然简单,但我每次回看都有新收获。它用最简单的题干,把“查找、去重、双指针、排序、复杂度权衡”这些算法最基础的概念串在了一起。刷题不是目的,理解和迁移才是。下次遇到类似求交集、求公共元素、判断包含关系的问题,你能第一时间想起这题里的几种思路,并且根据场景快速选择最合适的方案,那这题才算真正刷透了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询