先聊聊为什么这道题值一百题复盘。两数之和是LeetCode上提交量最高、最像"算法世界入口"的一道题,但说实话,很多人刷到几十题之后就把它忘了——毕竟它只是第一题,简单得不像话。我刷到第一百题的时候回头重做了一遍,才发现当初只是把答案背下来,并没有真正理解这道题背后牵出的哈希表、时间空间权衡、以及边界处理这些贯穿整个算法学习的核心主线。
这篇文章不是单纯给你一个可以AC的题解,而是站在刷完一百题之后的位置,重新拆解这道题的三层解法演进、哈希表的本质逻辑、变体题目的延伸方向,以及复盘一百题过程中我自己踩过的一些真实坑。适合刚开始刷题、正在刷前200题、或者刷到一定数量但觉得"记不住、不会举一反三"的读者。
1. 为什么第一百题复盘选了两数之和:这道题的地位被严重低估了
先说一个反直觉的现象:两数之和虽然标着"简单",是无数人打开LeetCode之后做的第一道题,但它在整个算法题体系里的位置,远比它的难度标签要重要得多。它是哈希表在面试题里最高频的开场,也是几乎所有"查找类问题"的最小原型。
1.1 两数之和在LeetCode题库里的真实位置
两数之和的题号是1,这个编号本身就说明了它在题库中的地位。从统计上看,它是被提交次数最多的题目之一,被各大公司的面试题库反复收录,也是许多刷题计划里的第一个节点。它不像后面的LRU缓存、滑动窗口最大值那样上来就给你一个复杂的工程结构,而是用最简单的数学表达——数组里找两个数,让它们的和等于目标值——把"查找"这一核心操作塞到你面前。
正因为它简单,大多数人看一眼题目,用双层循环跑一次,AC了,然后就走了。但实际上,这道题的升级路径可以延伸出三数之和、四数之和、两数之和II(输入有序数组)、两数之和III(设计类)、以及后面那些更复杂的子数组和等于K的问题。每一道看起来全新的题目,底层都在复用它那套"用哈希表记录历史信息"的思路。
1.2 一百题之后再重做,我发现当初根本没读懂题
说实话,我第一次做两数之和的时候,就是暴力解跑通的,连哈希表的解法都是看了题解才写的。这其实说明了一个问题:AC不代表理解,跑通不代表掌握。我刷到第一百题时再回头做它,看到的是完全不同的画面——这道题玩来玩去就两个关键点:第一个是"你需不需要保留数组的原始顺序信息",第二个是"你要返回下标还是返回值"。这两个点决定了你会选哪条路,也决定了你在后续几道变体题里会不会卡住。
比如,如果题目改成"输入有序数组",那最优解就不是哈希表了,而是双指针,时间复杂度O(n)、空间复杂度O(1),比哈希表的空间开销更优。再比如,如果要求返回所有不重复的组合,那哈希表就很难处理"去重"这件事,排序加双指针才是常规解法。你看,同一种数学关系,因为输出要求的变化,解法体系完全不同。这才是这道题复盘价值所在。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从暴力到哈希:两数之和的三层解法演进
先上一段题目的完整定义:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案,但是数组中同一个元素不能使用两遍。
这题解法上的演进,本质上是一条"如何减少不必要的遍历"的思考路径。
2.1 暴力枚举:所有解法里最"老实"的一版
暴力解法的思路不需要任何额外存储:从头到尾固定第一个数,然后在它后面剩下的区间里找第二个数,逐个相加判断。
python复制def twoSum_brute(nums, target):
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
if nums[i] + nums[j] == target:
return [i, j]
return []
时间复杂度是O(n^2),空间复杂度是O(1)。这段代码本身没有什么理解门槛,但它带来了一个至关重要的观察:内层循环每次都要从头把剩下的数扫一遍,大量的计算被消耗在"查找"上——每一次都在傻乎乎地把数组重新看一遍,而没有任何记忆。
类比一下生活场景:你想在一个书架上找两本书,让它们加起来的页码正好是500。暴力做法是拿起第一本,然后从书架的剩下部分一本一本地比对页码,没找到就放下,再拿第二本,再从头到尾比对一遍。这个操作不仅累,而且极端低效。
2.2 哈希表优化:用空间换时间的经典交易
暴力解之所以慢,是因为每次查找都没有"记忆"。而哈希表解法做的第一件事,就是建立一个"记忆":一边遍历数组,一边把已经看过的数放进哈希表里,这样对于当前这个数,只需要去问哈希表"有没有我需要的那个另一半",查找消耗是O(1)。
python复制def twoSum_hash(nums, target):
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
这里有两个容易出错的细节,值得展开说说。
第一个是"同一个元素不能使用两遍"这个限制。这道题测试用例里不会让你看到这种异常情况,但你自己写的时候一定要想清楚:你是先查表再存当前元素,还是先存当前元素再查表。先查表再存,保证了当前元素不会和自己匹配。如果你把顺序写反了,遇到类似数组[3, 3]、target=6这样的用例,会返回[0, 0],而不是正确结果[0, 1]。这个顺序问题在第一次写的同学身上非常常见。
第二个是哈希表里存什么。题目要求返回下标,所以value必须存下标。但有一类变体会问"请返回这两个数本身",那么value存数值也行。这个看似简单的差异,会在后续很多哈希表题里反复出现——你需要明确"我在哈希表里记录的是历史下标,还是历史值?我为什么要记录这个信息?"搞清楚这两个问题,哈希表的构造就不会出错。
时间上,这个版本只遍历了一次数组,每次操作都是O(1),整体O(n);空间上引入了一个哈希表,最坏情况下要存n个元素,O(n)。这是一次标准的"用空间换时间"。
2.3 排序加双指针:换个角度把问题重新表达
哈希表的解法已经很快了,但它也有局限——如果题目要求你返回所有不重复的组合,哈希表需要额外的去重逻辑,处理起来很麻烦。这时候有一个完全不同的视角:先把数组排个序,然后用两个指针从两头往中间走。
python复制def twoSum_two_pointer(nums, target):
nums = [(num, idx) for idx, num in enumerate(nums)]
nums.sort(key=lambda x: x[0])
left, right = 0, len(nums) - 1
while left < right:
cur = nums[left][0] + nums[right][0]
if cur == target:
return sorted([nums[left][1], nums[right][1]])
elif cur < target:
left += 1
else:
right -= 1
return []
双指针的正确性来自排序后的单调性:左指针向右移动,和会变大;右指针向左移动,和会变小。你要找的和比当前两数之和大,那就左指针右移;比它小,就右指针左移。每次移动排除了一个数,所以最多走n步,时间复杂度O(n log n),空间复杂度取决于你排序时的额外开销。
这里有一个坑要注意:因为排序会打乱原始下标顺序,所以你需要在排序前把下标和值绑定在一起存。上面代码里的[(num, idx) for idx, num in enumerate(nums)],就是干这件事的。如果题目改成"返回两个数的值",那就不需要绑定了,直接对值排序就行。很多初学双指针的人在这里会晕,其实核心就是:告诉计算机你要输出什么,它就需要你保留什么。
那什么时候选双指针,什么时候选哈希表?我的经验是:只看一个解不看数组本身,哈希表思路直观、代码简单,首选;如果要返回全部不重复组合,或者题目要求不使用额外空间(比如"只能使用O(1)额外空间"),那排序加双指针就是更优解。两数之和这道题本身因为要返回下标,双指针其实不是最优,哈希表一步到位,但理解双指针这条路线,对你后面解三数之和至关重要。
3. 哈希表为什么值得被反复训练:先想清楚"查什么"再想"怎么查"
两数之和是哈希表类型题里最简单的一道,但它是理解哈希表在算法题里核心角色的最佳起点。这个核心角色用一个词概括,就是"记忆"。
3.1 从两数之和到子数组问题:哈希表的"记忆"功能被反复复用
两数之和这种"查找历史信息"的模式,在后面的题目里被套用了无数次。最典型的是和为K的子数组这道题。它要求统计有多少个连续子数组的和等于目标K。朴素做法枚举所有子数组,是O(n^2)。但用前缀和的思路改造之后,问题的形式变成了"当前前缀和减去之前某个位置的前缀和等于K",也就是"之前有没有出现过某个前缀和",这跟两数之和的"之前有没有出现过某个数"在逻辑上一模一样。
再看最长无重复字符子串。你用一个滑动窗口扫过字符串,同时用哈希表记录每个字符最后出现的位置。窗口左边界的移动依据,就是哈希表里某个字符的出现历史。它的底层逻辑,依然没有脱离"我遍历到当前位置时,需要借助哈希表查询历史状态"。
这些题目和两数之和表面上看毫无关系,一个是数组里配对,一个是子区间统计,一个是字符串窗口,但解法的骨架是同一个:遍历一个序列,用哈希表记录已经看过的信息,从而让当前位置的决策不需要回头扫描。这也是为什么很多人说"两数之和是哈希表的母亲题"。不夸张。
3.2 从算法题到工程实践:哈希表的应用远比题解更广阔
如果只把哈希表当做出题工具,就太可惜了。真实工程里的缓存系统(Redis里的字典)、数据库索引结构的一部分(哈希索引)、文件去重、日志聚合、分布式数据分片,全都有哈希表的影子。你在LeetCode上练的"用哈希表查历史",本质上练的是一种"在数据处理流程中快速定位目标记录"的思维方式。
举一个真实场景:你有一批用户日志,每行包含用户ID和访问时间,现在要统计每个用户在某个时间窗口内访问了多少次。最简单的做法就是遍历日志,用一个以用户ID为key、以计数器为value的哈希表累加。这个做法的核心代码和两数之和里"把数组元素往哈希表里塞"的操作,在思维上是同一个动作——用哈希表维护一个"从已知信息到所需结果"的映射。
所以我一直觉得,刷算法题并不是在背答案,而是在练一种"信息组织方式"。哈希表就是你遇到"需要快速查找历史记录"时的第一反应,而两数之和就是把这种第一反应刻进肌肉记忆的最佳练习。
4. 一百道题复盘,我在两数之和上看到的通用规律
刷完一百题再回头看,两数之和这种"入门题"反而能检验出很多学习方法的短板。这一节不局限在题目本身,聊聊我在复盘过程中总结出的三个通用规律,适合所有刷题阶段遇到瓶颈的人。
4.1 一题多解的价值:别人教你一种,你自己要会推另外几种
两数之和是少见的一道题可以同时容纳三种"几乎完全不同"的解法的题目。暴力、哈希、排序加双指针,三者在时间、空间、适用场景上各有优劣。真正的学习发生在哪里?发生在你做完一种解法之后,主动去问"如果我的数据是有序的怎么办?如果我要返回所有组合怎么办?如果我只能用O(1)空间怎么办"。
这比连着做十道题有用得多。因为刷题数量本身不产生能力,产生能力的是你在每道题上建立的"决策树"。我之前带过的新人,很多是见了题就写,写完AC就看下一道,三个月刷了300道,遇到一道经典的题目变形还是没思路。问题就出在他们没见过"多解",也没训练过"为什么选这个解"。
4.2 边界条件:哈希表版本查表与存值的顺序,是所有哈希表题的"第一道坎"
两数之和解决的一个隐藏问题,就是"当前元素不能和自身匹配"。这个约束如果不细想,很容易失手。我在很多哈希表的题里都见过类似的"顺序陷阱",尤其是设计类题目,比如设计一个支持重复key的哈希表、设计一个O(1)时间复杂度的LRU缓存,它们的注意点里都有"先读旧数据再写新数据"这一条。
我自己在复盘时一直在用一个小技巧:把题目里的边界条件全部列出来,然后用一组针对性的测试用例去验证。两数之和的边界条件包括:数组长度为2、存在负数、元素重复出现、target是两倍于某个元素的值。每组边界条件,对应一个可能出bug的场景。
4.3 从"会做"到"会讲":复盘输出的内化效应
一百题复盘过程中我体会最深的是:会做一道题,和能把这道题讲清楚,完全是两个层次。你可以在看完题解之后把代码默写下来,看起来很会,但如果你被问到"为什么这个解法是O(n)的""为什么哈希表查找是O(1)""如果数组里有重复数字怎么办",回答不出来,说明理解还是浅的。
复盘时我习惯做三件事:第一,把所有解法的复杂度推导写一遍;第二,用自然语言把解法逻辑描述一遍,就像在跟同事讲方案;第三,去题解区找最优解和别人的不同思路,确认自己有没有盲区。两数之和作为一道小题,也值得走完这三步。当你能够不看代码,把这个过程完整讲出来,才算是真正吸收。
5. 两数之和的变体与延伸:一道题能长出多少新题目
既然是一百题复盘,光说两数之和本身肯定不够。这节课最值得讲的是它延伸出的变体。从前面的热度搜索也能看到,很多人刷完最热门的100题之后,继续在找各种变体和周赛题来练手,而两数之和正是最热门百题系列里的第一块基石。
5.1 两数之和的经典变体地图
我整理了一下,从两数之和出发,至少能长出一整棵"题目亲戚树":
| 变体 | 核心变化 | 解法方向 |
|---|---|---|
| 两数之和 II - 输入有序数组 | 数组有序 | 双指针(O(n), O(1)) |
| 三数之和 | 找三元组,不重复,返回组合 | 排序+双指针 |
| 四数之和 | 找四元组,不重复 | 排序+双指针+外层固定两位 |
| 和为K的子数组 | 下标连续,统计数量 | 前缀和+哈希表 |
| 两数之和 IV - 输入BST | 二叉树版本 | 中序遍历后双指针,或哈希表 |
| 两数之和(设计类) | 支持add/find操作 | 数据结构设计,哈希表维护频率 |
这里要特别说一句:变体和原题之间的关系,不是"改了个数字",而是"约束条件变了,数据结构的选择随之变化"。例如两数之和II把数组改成有序,就用不上哈希表了,因为双指针更省空间。三数之和要求返回不重复的三元组,哈希表去重困难,所以排序加双指针成为主流。如果你只背了原题的哈希表解法,遇到这些变体就会觉得"明明都是两数之和,怎么换个条件就不会了"。真相不是你不会,而是你没有建立"约束-解法"之间的映射关系。
5.2 从两数之和到Google面试级追问
再说一个有意思的延伸。很多公司在面试时会把两数之和当成开场题目,然后不断升级。先是基础哈希表版本,接着追问"如果数组非常大,放不进内存怎么办",这属于大数据场景下外部排序和分块的思路,和内存型解法已经完全不同了。再追问"如果元素有重复,所有答案都要打印出来",那就是排序加双指针配合去重。如果再追问"你能不用额外的存储空间吗",那就要回到双指针,但前提是数组有序。
这一层层的追问,本质上考验的是候选人有没有把"数据规模、存储限制、输出要求"这些条件纳入决策的习惯。两数之和一个看似无脑简单的题目,其实可以承载面试官对工程思维的一系列考察。所以如果你正在准备面试,别因为它是第一题就轻视它,把它做透,远比囫囵吞下十道中等题更有安全感。
5.3 一百题复盘的题目选择思路:优先重做"入口题"和"模型题"
最后分享一下我这轮复盘的选题方法。一般来说,一百题之后不是盲目开新题,而是挑"入口题"和"模型题"重做。入口题指的是那些题号靠前、被很多人做过的题,比如两数之和、有效的括号、合并两个有序链表。模型题指的是能够代表某一类解法的题目,比如LRU缓存代表哈希表+链表组合、二叉树的层序遍历代表BFS。
选择这些题来复盘,是因为它们的解法结构足够基础,其他高阶题往往是在它们上面做组合和变形。如果你能够把两数之和这类"模型题"的解法逻辑、复杂度、边界条件都重构一遍,后面的中等题和难题就会顺很多。这比继续往下刷新题,效果要扎实得多。
6. 复盘一百题之后,我再做两数之和时会注意什么
最后用一小节总结一下,现在我再拿到两数之和这道题,脑子里会瞬间过掉的检查清单。这个清单不是背题,而是一套重复使用的思考框架。
第一步,先看输出要求。是返回下标还是返回值,这决定哈希表里存什么,也决定双指针版本排序时要不要保存绑定信息。
第二步,再看输入特征。数组有序吗?数字有重复吗?数字范围有上限吗?如果有序,优先想双指针;如果无序且要求下标,哈希表是首选。
第三步,最后确认时间空间约束。能接受O(n)空间,选哈希表;不能接受额外空间但能排序,选排序加双指针;排序的代价O(n log n)能不能接受,也要问自己。
这三步走完,不管题目怎么变形,你都能有一个清晰的落点。很多人刷了一百题还觉得"记不住"的原因,就是缺了这套决策框架,只有散落的代码片段。
我个人在整个复盘过程中最大的体会是:一道题真正的价值不在于它本身有多难,而在于它能在你的知识体系里牵出多少条连线。两数之和被放在第一题,不是随机的。它牵出了哈希表的核心思想,牵出了暴力到最优的优化路径,牵出了双指针的单调性根基,还牵出了前缀和、滑动窗口、三数之和等一系列题目的共同原型。
如果你现在也在刷LeetCode,不管刷到第几题,我建议你挑一个固定节点停下来,把所有做过的题按"模型"重新归类,找出其中的入口题,静下心重做一遍。你会发现,原来很多东西并不是真的忘了,只是当时没有把它们放进一个统一的框架里。
两数之和是这条路的第一块基石,一百题之后回头看,依然立得住。
