1. 为什么HOT 100把“两数之和”放在第1位1.1 一道覆盖两大基础能力的入门名题LeetCode HOT 100里的第1题就是这道“两数之和”。哪怕你不刷题只要接触过算法面试十有八九都见过它的模样给一个整数数组nums和一个目标值target让你找出和恰好等于target的两个数返回它们的下标。这道题的地位很特殊。它不像那些动辄几千字的困难题读题三十秒就能理解但它又不像纯输出Hello World那样毫无营养——从最粗暴的双重循环到漂亮的哈希表一次遍历中间要跨过整整一个复杂度台阶。新手能从中练到最基础的数组遍历和边界处理有经验的开发者也能从中回味“枚举所有情况”和“用空间换时间”这两种思维模式的取舍。我见过很多人的刷题记录第一道题几乎都是它。HOT 100把它放在榜首都不是没有道理的难度温和、思路典型、延伸性强非常适合作为整个刷题计划的起跑线。而它背后的“补数查找”套路在后面几十道题里还会反复出现。这篇文章我就从零开始拆把暴力解法的写法、哈希表解法的设计逻辑、以及那些没人提醒就容易踩的坑一次讲清楚。1.2 破题之前先把题目和约束条件读准刷题第一件事不是写代码是确认题目到底在问什么。这道题的核心约束其实有四条输入是一个整数数组nums和一个整数target需要找两个不同下标的元素让它们的和等于target返回的是这两个元素的下标题目保证每种输入只会对应一个有效答案。第四条约束非常关键它意味着你不需要处理“多组答案”或者“无解”的复杂情况代码走到一半找到答案直接返回即可。这也是为什么这道题会被归类为简单题——它的核心难点不在“找到所有可能性”而在“用聪明的方式快速找到唯一解”。很多初学者拿到题目就直接写双重循环去了这没有错但写之前最好先在纸上画一画如果数组长度是n暴力枚举所有“两个下标”的组合一共有多少种想清楚这个问题你才能真正理解为什么后面要优化成哈希表。2. 暴力解法先把最朴素的路径走通2.1 双重循环怎么写得干净又正确先看最直观的解法两层for循环外层固定一个数内层遍历剩下的数检查两个数相加是否等于target。这是绝大多数人第一次 AC 这道题时会写的版本class Solution { public int[] twoSum(int[] nums, int target) { for (int i 0; i nums.length; i) { for (int j i 1; j nums.length; j) { if (nums[i] nums[j] target) { return new int[]{i, j}; } } } return new int[0]; } }有几个细节值得注意。第一内层循环的j一定要从i 1开始而不是从0开始。如果从0开始你会去检查i和j相同的情况也就是同一个元素自己加自己这直接违反“两个不同下标”的约束而且还会把(i, j)和(j, i)两个组合各检查一遍白白浪费一半时间。第二循环变量最好写成i nums.length而不是i nums.length - 1因为后者读起来容易让人误以为数组长度和最后一个下标的关系写错边界就是典型的 off-by-one 错误。第三当题目保证有解时函数体内可以直接在命中条件里return如果万一没有解循环结束后要返回一个空数组new int[0]保证编译器路径完整。2.2 暴力法的时间复杂度为什么只能用来入门暴力解法的时间复杂度是 O(n²)空间复杂度是 O(1)。这里把复杂度推导过程写清楚面试时很可能会被问到外层循环一共遍历n次对于每个i内层循环从i 1遍历到n - 1。所以比较的总次数是(n - 1) (n - 2) ... 1 n * (n - 1) / 2在大 O 记号下我们只保留最高阶项忽略常数系数于是得到O(n²)。这在实际运行中意味着什么如果n 1000比较次数是 50 万左右现代机器上毫无压力但如果n 100000比较次数就是 50 亿量级即使某种语言跑得再快在 LeetCode 的评测环境下也很容易出现 Time Limit Exceeded。HOT 100 里的测试数据规模通常不会太小所以这道题虽然暴力解法能过但真要追求稳妥还是要走哈希表。我自己刷这题的时候第一遍就只写了暴力的 7 行代码当时觉得“也不难嘛”。直到后来面试官追问如果数组长度涨到一亿这个解法还能用吗我才意识到所谓刷题不只是 AC 一道题而是要把不同数据规模下的表现也考虑进来。3. 哈希表解法一次遍历把查找降到O(1)3.1 核心思路与其枚举两个数不如直接找“补数”暴力法慢在哪里慢在内层循环把“剩下的所有元素”都重新扫了一遍。但如果换个角度想当我站在nums[i]面前时我真正想知道的只有一件事——之前有没有出现过target - nums[i]这个数如果有它的下标是多少target - nums[i]就是当前元素的“补数”。与其把数组从头到尾翻一遍去找补数不如把已经见过的元素存进一个哈希表以“元素值”为 key、以“下标”为 value。这样每次只要 O(1) 的时间就能在哈希表里回答“补数是否存在在哪”。生活里有个特别贴切的类比你在一堆名片里找某个电话号码。暴力做法是一张一张翻哈希表做法是先把名片按姓氏整理成通讯录直接在通讯录里查。第一次翻和第一百次翻暴力做法都要从头来而通讯录查询每次都是按索引定位。3.2 先查后存还是先存后查这里有个经典陷阱哈希表的思路听上去简单但新手写出代码后最容易翻车的就是一个顺序问题在每一次循环里到底是先put当前元素进哈希表还是先检查补数是否在哈希表里正确答案是先查后存。为什么用一个例子说明。假设nums [3, 2, 4]target 6正确答案是下标[1, 2]因为2 4 6。如果写成“先存后查”for (int i 0; i nums.length; i) { map.put(nums[i], i); // 先把当前元素存进去 int complement target - nums[i]; if (map.containsKey(complement)) { return new int[]{map.get(complement), i}; } }遍历到i 0时先把3 - 0存进哈希表然后补数target - 3 3查表发现 3 就在里面于是返回[0, 0]——也就是把同一个nums[0]用了两次。这显然违背题意而且恰好会让测试用例挂掉。如果先查后存for (int i 0; i nums.length; i) { int complement target - nums[i]; if (map.containsKey(complement)) { return new int[]{map.get(complement), i}; } map.put(nums[i], i); // 查完再存保证不会用到自己 }遍历到i 0时补数是 3哈希表还是空的查不到于是把3 - 0存进去遍历到i 1时补数是 4哈希表里没有 4把2 - 1存进去遍历到i 2时补数是 2哈希表里已经存了2 - 1于是返回[1, 2]。正确。这个顺序问题非常经典我在帮别人 review 代码时经常看见。它不是玄学核心逻辑就是“当前元素在还没进哈希表之前它不能自己配自己进了哈希表之后这个记录是给后面的元素用的”。3.3 Java与Python实现对照注释里全是细节哈希表解法在不同语言里的实现思路完全一致只是语法不同。我平时面试习惯用 Java刷题偶尔用 Python两个版本都贴出来。Java 版本class Solution { public int[] twoSum(int[] nums, int target) { MapInteger, Integer seen new HashMap(); for (int i 0; i nums.length; i) { int complement target - nums[i]; if (seen.containsKey(complement)) { // 注意返回顺序先补数下标再当前下标 return new int[]{seen.get(complement), i}; } seen.put(nums[i], i); } return new int[0]; } }Python 版本class Solution(object): def twoSum(self, nums, target): seen {} for i, num in enumerate(nums): complement target - num if complement in seen: return [seen[complement], i] seen[num] i return []两个版本里都藏着同一个设计选择哈希表的 key 是数组的值value 是数组的下标。千万不要把方向弄反。如果你用下标做 key、值做 value那查表的时候你只知道某个下标存了什么值却没办法回答“某个值是否存在、它的下标是什么”整个思路就废了。时间复杂度方面哈希表解法是 O(n)空间复杂度也是 O(n)多出来的空间就是那张存了 n 个键值对的哈希表。严格一点说哈希表在哈希冲突极端严重时查找会退化所以更严谨的说法是“平均情况下 O(n)最坏 O(n²)”。但在普通面试和 LeetCode 测试数据下平均情况就是常态。4. 实操中的高频坑与排查技巧4.1 重复元素把哈希表覆盖了怎么办很多人写哈希表解法时会自然而然地担心如果数组里有重复元素比如nums [3, 3]target 6那第二个 3 是不是会把第一个 3 的下标覆盖掉先查后存的写法天然规避了这个坑。遍历过程是这样的i 0时补数是 3哈希表空查不到于是存3 - 0i 1时补数是 3哈希表里已经存在3 - 0直接返回[0, 1]。整个过程根本轮不到“覆盖”发生。真正会出问题的是另一种写法先把整个数组一次性全部put进哈希表然后再遍历一遍数组去查。以nums [3, 3]为例最终哈希表里只会留下3 - 1因为后存入的 key 相同value 把前一个覆盖了。遍历i 0时查到的是1返回[1, 0]顺序反了如果遍历到i 1时还去查查到的还是1返回[1, 1]这就是彻底错误——同一个下标被用了两次。所以我的建议很直接这道题不要“先 build 再 query”坚持“边遍历边查询边存入”。这也是很多官方题解推荐的标准写法。4.2 边界条件与返回值越短越要小心题目虽简单边界条件依然要处理。我列一下我在代码里一定会检查的几种情况数组长度为 0 或 1不可能存在两个不同下标能相加直接返回空数组。数组为null在 Java 里nums.length会抛NullPointerException需要先判空。循环正常结束但没有返回说明输入违反了“一定有解”的假设此时需要返回空数组兜底。关于返回值理解题目要“下标”而不是“值”非常重要。有人会把数组元素本身返回出来这在思考层面对了但题目要的是索引测试机比对的是索引写错一个字都是 WAWrong Answer。还有一个容易被忽略的细节LeetCode 里返回的数组下标是 0 -based也就是从 0 开始数。如果你习惯 1-based 思维写成了i 1那也是错。4.3 常见问题速查表我把自己见过的、以及在社区里经常被问到的问题整理成了一张表方便你对照自查。问题场景原因处理方法返回[0, 0]先存后查导致当前元素自己配自己改为先查补数再存当前元素返回[1, 1]之类的相同下标一次性 build 哈希表后遍历重复 key 覆盖 value采用边遍历边存的标准写法数组为空/长度为 1无法形成两个不同下标提前判空返回空数组运行超时暴力解法在 n 较大时是 O(n²)改用哈希表降到 O(n)数组下标越界循环里用了i 1但没判断上限内层循环j从i 1开始且 nums.length使用了排序后双指针但返回下标错乱排序会打乱原下标如需返回原始下标优先哈希表如题目只返回值可用双指针这张表里的问题我基本都在实际测试里踩过或见过。尤其是“先存后查导致自己配自己”这个坑只要面试官把题目改编成“如果数组里有一个元素恰好等于 target 的一半”没想清楚顺序的人立刻露馅。5. 从一题到一类用哈希表思维打通更多题目5.1 两数之和的三个常见变体这道题真正的价值在于它是一个思维模板。换了条件之后解法经常也要跟着变变体一数组是升序的且只要返回两个数的值。这道经典题对应 LeetCode 167可以用双指针从数组两端往中间走因为数组有序指针之和与target的关系可以决定指针移动方向时间复杂度 O(n)空间复杂度 O(1)。变体二数组里可能存在多组答案要求列出所有不重复的组合。典型的就是“三数之和”它需要先排序再用双指针去重。注意一旦要求“多组”和“不重复”哈希表写法反而要绕排序是更好的切入点。变体三连续子数组的和等于 target要求统计个数。这是“和为 K 的子数组”问题思路从“两数之和”扩展成了“前缀和之差”sum[j] - sum[i] k于是可以用哈希表保存“前缀和 - 出现次数”一次遍历完成统计。这三个变体覆盖了 LeetCode 里一大票“看起来像两数之和”的题。你只要把这道题吃透后面遇到三种变形时完全可以套用同一套思考框架先想清楚“我要找什么”再想清楚“这个目标能不能存在哈希表里”。5.2 哈希表在算法题中的三种典型用法通过这一题你其实可以总结出哈希表在算法题里的三种高频用途这对后续刷题非常有帮助用途典型场景代表题目判断元素是否存在查重用 Set 或 Map 记录已出现元素最长连续序列、存在重复元素记录元素到下标的映射用 Map 保存“值 - 位置”便于 O(1) 回溯两数之和、字母异位词分组记录前缀状态和统计值用 Map 保存“前缀和 - 次数 / 最早下标”和为 K 的子数组、连续数组这第三种用法尤其重要。很多“连续子数组求和”的题你暴力解是 O(n²)但一旦引入前缀和 哈希表就变成了 O(n)。这不是某种奇技淫巧而是把两数之和中“补数查找”的思想搬到了连续区间上。换句话说两数之和给的不是一道题的答案而是一种“如何把查找变成 O(1)”的通用心智模型。6. 我反复刷这道题之后的几点体会这道题我前前后后刷了不下五遍每一遍都会有新的感受。第一遍我只能写出暴力解法第二遍看了题解才懂哈希表第三遍开始注意到“先查后存”这个细节第四遍能不看题解直接给出先查后存的 Java 代码第五遍则开始主动思考它的变体和扩展。我最大的体会是一边遍历一边建表的写法本质上是把“未来的查询需求”预支给了“当前的历史记录”。这个思维在几乎所有使用哈希表的题目里都通用。你不需要一次性把所有信息全部预处理完很多时候边处理边记录反而更容易避开边界问题。给正在刷题的读者三个建议第一遍可以允许自己用暴力法 AC但 AC 之后一定要追问一句这个解法的时间复杂度和空间复杂度分别是多少在什么数据规模下会挂第二遍刷的时候只允许自己写哈希表解法并且要在心里把“先查后存”的理由讲清楚讲不清楚就说明还没吃透。刷完这道题顺手把三数之和、和为 K 的子数组、字母异位词分组这几题拉出来对比一下看看哪道题用了哈希表的哪种用法。LeetCode HOT 100 的顺序其实很讲究第一题给了你一个非常轻量的起点。如果能从这道题里真正收获“如何把 O(n²) 想成 O(n)”的思维方式那后面几百道题的路走起来会顺很多。