ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LeetCode热题100第二遍怎么刷?两道高频题拆解与实战策略

LeetCode热题100第二遍怎么刷?两道高频题拆解与实战策略 LeetCode热题100二——没错我就是在说第二遍。第一遍刷的时候我犯了一个所有新手都会犯的错把热题100当成“题库”来刷每天机械地做十道做完标记然后下一题。结果就是题量上去了脑子里剩下的东西却没多少。等到面试官把“基本计算器”换个壳子把“爱吃香蕉的狒狒”改几个参数我就歇菜了。这篇文章记录的正是我第二轮啃LeetCode热题100时的真实方法、踩过的坑以及两道让我反复琢磨的高频题完整拆解。如果你也准备认真过一遍这份经典清单前半部分讲策略后半部分讲硬核代码你可以挑着看。1. 为什么热题100值得刷第二遍1.1 热题100到底是什么先说清楚一个容易被忽略的事实。力扣的“热题100”不是随便选的100道题它是根据海量用户提交、面试反馈、企业笔试数据加权汇总出来的高频题集合。换句话说这100道题基本覆盖了算法面试里80%以上的核心考点数组、链表、哈希表、双指针、滑动窗口、栈、堆、二分、动态规划、回溯、图论、贪心……几乎每种算法思想都在里面占有一席之地。所以热题100的正确用法不是“刷完就毕业”而是把它当成一个算法能力的基准线。第一遍刷目的是对各类题型混个脸熟知道“哦原来这种题可以用滑动窗口”“这种题本质是二分答案”。第二遍刷目的是把这些题目背后的方法论抽象出来做到看到题目就能条件反射地想到算法方向。第三遍刷才是为了面试前的手感保持。我见过不少人刷了三遍热题100代码都能背下来了但面试依然挂。原因很简单他记住的是答案而不是思路。第二遍的意义恰恰就在这里——它不是让你把题背得更熟而是让你把“为什么这道题这么解”彻底想透。1.2 第二遍和第一遍的区别第一遍刷题的状态大概率是这样的打开题解照着敲一遍通过心满意足。第二遍刷题如果还是这个状态那基本等于白刷。我给自己定的第二遍标准是三个“能”能不看题解独立写出AC代码。能说出这道题的时间复杂度和空间复杂度并给出推导过程。能举出一个这道题的变体场景以及对应的解法调整方式。举个例子第一遍刷“最大子数组和”LeetCode 53的时候我只知道背Kadane算法的模板cur max(cur nums[i], nums[i])然后每次更新ans。到了第二遍我会强迫自己想清楚为什么cur小于0的时候重置是合理的因为负数对后续子数组的贡献一定是负的留着它不如从当前位置重新开始。这个思考过程才是刷题真正有价值的部分。如果你现在准备开始第二遍先给自己定个目标不求快求透。宁可一周只吃透五道题也不要一天囫囵吞枣二十道。2. 第二轮刷题的战术安排2.1 按专题刷不按顺序刷第一遍刷题大多数人习惯按照题号或者列表顺序来因为那样省心。第二遍如果还这么干效率会非常低。热题100的列表顺序本身是打乱的今天你做一道哈希表明天做一道动态规划思维切换的损耗很大而且不利于总结规律。我第二轮的做法是先把100道题按专题归类数组与哈希一组双指针与滑动窗口一组栈与单调栈一组二分答案一组二叉树一组动态规划一组回溯一组图论一组。然后每个专题集中一到两天的时间全部过一遍。这样做的好处非常明显。当你连着做十道滑动窗口题的时候你会自然产生一种“手感”什么样的场景适合滑动窗口无非是“连续子数组”“子串”“满足某种条件的最长/最短区间”。而当你把这种手感带到面试里看到题目的第一反应就会快很多。2.2 主动写题解而不是收藏题解第二轮刷题的时候我给自己加了一条硬性规则每道题刷完必须用自己的话写一遍题解哪怕只有三五行。不用发出去写在本地笔记里就行。写题解的目的是逼自己把“我好像懂了”变成“我真的懂了”。这里有个很实用的技巧用费曼学习法。写完题解之后假装对面坐着一个完全不懂算法的人用最直白的话给他讲一遍这道题。如果讲着讲着发现自己说不清某个步骤为什么要这么做恭喜你找到了薄弱点。我第二轮大概写了六十多篇简短的笔记现在回头翻很多当时觉得“很简单”的题笔记里其实记录了不少当时没想透的问题。这些笔记后来在面试前复习的时候价值巨大比重新刷一遍题高效得多。2.3 把复杂度算清楚再往下走这是很多人忽略的一点。很多人的代码能AC但你问他“这题时间复杂度为什么是O(n log n)”他支支吾吾说不出来。面试的时候代码写对了只是及格线能说清楚复杂度分析才是加分项。我在第二遍刷题时养成了一个习惯每道题AC之后必须额外花两分钟在代码注释里写下复杂度分析。比如“爱吃香蕉的狒狒”这道题二分外层是O(log M)M是香蕉堆里的最大数内层遍历piles数组是O(N)所以总复杂度是O(N log M)。这个推导写在注释里下次复习的时候一眼就能看到不用再从头算。3. 基本计算器栈专题的试金石3.1 题目到底在考什么基本计算器LeetCode 224在热题100里的地位很特殊。它表面上是“实现一个简单的计算器”但实际上它考察的是三个核心能力栈的运用、状态管理、边界处理。题目的要求是给定一个字符串表达式包含数字、、-、括号和空格计算出结果。注意没有乘除没有幂运算这降低了难度但也让题目的重点更集中——括号的优先级处理。第一次做这道题的时候我差点把eval()拍上去。当然面试里这么干等于自杀。后来我仔细想了它的本质在没有乘除的情况下表达式求值的关键就是如何处理括号带来的“符号翻转”。一个括号前如果有个负号括号内部的所有符号都要反过来。这时候栈就派上用场了——你可以在遇到(的时候把当前累积的结果和当前符号压栈进入括号内部时重新累计遇到)的时候再把栈里的符号和结果取出来组合。3.2 为什么用栈而不是递归这道题很多人第一个想到的解法是递归。遇到括号递归地算括号里面的值。这确实能解但我更推荐栈解法原因有两个。第一递归的隐式栈和显式栈本质是一样的但显式栈的代码更容易控制也更容易处理括号嵌套的复杂情况。第二面试的时候能熟练地用一个数据结构解决问题比递归嵌套的写法更让面试官放心。递归在处理很深层的括号时还有爆栈风险虽然这道题的测试数据还没到那个程度但习惯用栈总没错。栈解法的核心思路是这样我们用两个变量result保存当前已经计算完成的部分sign保存当前数字的符号。遇到数字就累加成一个完整的数注意多位数字遇到或-就把前面的数字乘以符号累加到result里然后更新符号。遇到(就把当前的result和sign压入栈然后重置result和sign去计算括号内的值。遇到)把括号内的result结算完毕先乘以栈里弹出的符号再加上栈里弹出的外层结果。3.3 代码实现与边界细节下面是我最终稳定AC的版本Python实现def calculate(s: str) - int: stack [] result 0 sign 1 current_num 0 for c in s: if c.isdigit(): current_num current_num * 10 int(c) elif c : result sign * current_num current_num 0 sign 1 elif c -: result sign * current_num current_num 0 sign -1 elif c (: stack.append(result) stack.append(sign) result 0 sign 1 elif c ): result sign * current_num current_num 0 result * stack.pop() # 弹出括号前的符号 result stack.pop() # 弹出括号前累加的外层结果 result sign * current_num return result这里有几个特别容易踩的坑我一个个说。第一个坑多位数字。字符串里的数字可能是“123”这样的多位数如果只按字符一位一位处理就会算出1236而不是123。所以必须用current_num current_num * 10 int(c))这种方式累积。第二个坑空格。题目允许空格存在但上面的写法直接跳过了空格——因为空格不属于任何分支循环体里什么都不做。这个处理非常简洁但如果面试时你用的是别的语言注意字符串遍历时空格的处理逻辑。第三个坑括号前的符号。看这行result * stack.pop()。假设表达式是1 - (2 - 3)遇到(之前sign已经被-更新成了-1所以我们把-1压栈。算完括号内得到-1即2减3的结果是-1注意在括号内我们重新从result0开始最终resultsign*current_num是-1然后乘以栈里弹出的-1变成1再加上栈里弹出的外层结果1得到2。手动验算一下1 - (2 - 3) 1 - (-1) 2完全正确。这个小逻辑看起来简单但我第一次写的时候把stack.append(result)和stack.append(sign)的顺序写反了导致弹出的时候完全错乱。后来我给自己固定了一个规矩压栈时先压result再压sign弹出时先取sign再取result写成代码就是先stack.pop()出符号再stack.pop()出结果。这个规矩一旦定下来就再没出过问题。4. 爱吃香蕉的狒狒二分答案的模板题4.1 为什么想到二分LeetCode 875“爱吃香蕉的狒狒”是一道非常经典的二分答案题。题目描述很简单一堆香蕉分成若干堆每堆有piles[i]根狒狒每小时只能吃一堆且吃的速度是k根/小时。如果一堆剩余不足k根它会吃完这一堆但不再吃下一堆。问在h小时内吃完所有香蕉的最小速度k是多少。第一次看到这道题我的第一反应是暴力枚举从k1开始试直到找到一个速度能满足要求。但一看数据范围piles[i]最大能到10^9暴力肯定超时。这时候就要想到二分的经典应用场景答案在一个单调的区间内且我们能写一个判断函数can_finish(k)来判断某个速度是否可行。速度k越大吃完所需的总时间越短所以“能否在h小时内吃完”这个属性对于k是单调的——k小的时候可能不行k大到一定程度一定行。我们要找的正是这个“临界点”也就是最小的可行k。4.2 判断函数怎么设计判断函数的逻辑很简单遍历所有香蕉堆计算在速度k下吃完每一堆需要多少小时然后求和判断是否小于等于h。这里有个核心细节每堆的耗时怎么算是piles[i] / k吗不是。因为题目说了如果一堆剩下的香蕉少于k根狒狒吃完了这一堆但不会在这个小时内再去吃下一堆。也就是说每堆耗时是向上取整的ceil(piles[i] / k)。向上取整在编程里的实现有个经典技巧hours (p k - 1) // k这个写法避免了浮点运算直接用整数搞定。我在实际刷题中发现很多人直接用math.ceil(p / k)也能AC但浮点运算在大数场景下会有精度隐患而且性能不如整数运算。面试的时候写(p k - 1) // k一句话就能解释清楚很加分。判断函数完整如下def can_finish(piles, k, h): hours 0 for p in piles: hours (p k - 1) // k return hours h4.3 二分边界与死循环问题二分答案的代码模板很固定但边界条件稍有差池就会死循环或者答案错误。我最终采用的版本是“左闭右开”的变形def minEatingSpeed(piles: list[int], h: int) - int: left 1 right max(piles) while left right: mid (left right) // 2 if can_finish(piles, mid, h): right mid else: left mid 1 return left来解释一下为什么这么写。下界left为什么是1而不是0因为狒狒每小时至少要吃掉一根香蕉速度为0没有意义而且除以0会直接报错。上界right为什么是max(piles)因为如果速度等于最大堆的香蕉数那么每个小时最多吃掉一整堆耗时就是堆的数量这是最坏情况下也能在len(piles)小时内完成的速度。题目保证h len(piles)所以max(piles)一定是一个可行解。再来看二分的收缩规则。当mid可行时说明速度还可以更小所以我们把右边界收缩到mid即right mid。当mid不可行时说明速度必须更大且mid本身被排除了所以left mid 1。这个“可行时右边界收不可行时左边界进一”的模式是二分答案找最小可行解的标准写法。这里最容易出的问题是什么时候会出现死循环如果left和right相邻比如left3, right4mid(34)//23。如果mid3不可行left变成4循环退出。如果mid3可行right变成3循环也退出。因为mid (left right) // 2在整数除法下永远小于right所以right mid必然会让区间变小不会死循环。这个结论我在纸上推演了好几遍写死循环恐慌症的朋友可以放心。4.4 复杂度视角下的最优性这道题的时间复杂度很多题解直接写O(N log M)但没解释清楚N和M分别是什么。N是piles数组的长度也就是香蕉堆数M是max(piles)也就是单堆香蕉的最大数量。二分外层跑O(log M)轮每轮判断函数要遍历整个数组O(N)所以总复杂度O(N log M)。这个复杂度到底够不够好假设piles长度是10^4最大堆是10^9那么log M大约是30总操作量在3×10^5级别完全在1秒时限内。这也是为什么“二分答案”这类题目在热题100里频繁出现——它考察的不仅是二分模板更是对“单调性”这一核心性质的洞察力。你写的不是二分你写的是“猜答案然后验证”。5. 常见问题排查与面试避坑5.1 刷题时反复出现的五个问题第二轮刷热题100的过程中我总结了一些出现频率极高的问题这里直接给出一张排查表方便你对号入座。问题现象可能原因解决办法代码本地跑得对一提交就超时复杂度太高往往是无意中嵌套了循环先检查是否有内层循环可以提前退出再考虑换更优的数据结构二分题答案差1边界收缩条件写错套用“可行收右、不可行进左”的模板并打印mid验证栈题括号匹配出错压栈顺序与弹出顺序不一致固定压栈和弹出的顺序规则见上文基本计算器部分滑动窗口题少了某个边界条件右侧指针移动和左侧指针收缩的时机不对先明确窗口的约束条件再用“先扩张右侧、再收缩左侧”的框架动态规划题答案对但解释不清只记住了递推公式不理解状态定义强行用文字写一遍dp[i]到底代表什么写完再做题这张表里前两条是我自己踩得最多的坑。特别是二分差1的问题我有一段时间每到一道新题都要在纸上手动模拟一次left和right的移动后来才彻底摆脱了靠猜的坏毛病。5.2 现场做题的时间分配建议面试现场写算法题时间分配很有讲究这是我在第二次刷题时才开始意识到的。很多人一上来就埋头写代码写得飞快然后发现思路错了全部推倒重来。正确的做法是前5分钟跟面试官对齐题意确认输入输出、边界条件、数据范围中间5到10分钟讲思路包括复杂度和为什么选择这个算法最后10到15分钟写代码和测试。这里有一个容易被忽视的技巧一边写代码一边用嘴说。不是让你自言自语而是把每一行代码的意图用简洁的语言表达出来比如“这里我初始化栈用来保存括号前的符号”。这样做的好处是面试官能实时跟上你的思路如果哪里有偏差他能及时纠正而不是等你写完了才发现方向错了。我在第二轮刷题时故意用这种方式练习了一个月效果显著。5.3 用周赛检验学习成果说到检验学习成果最近正好赶上LeetCode周赛430我建议大家在第二轮刷题的中后期每周末参加一次周赛。周赛的题目不会直接从热题100里出原题但它们的核心思想几乎都来自热题100的那些经典模型。我第430周赛的体会是前两道题用到的滑动窗口和排序技巧基本就是热题100里反复出现的套路第三道题稍微复杂一点但本质上是二分答案和模拟的结合跟“爱吃香蕉的狒狒”的思维路径很接近。这说明什么说明热题100刷透了周赛里你至少能稳定做出前两题第三题也有思路可循。周赛的意义不在于排名而在于让你在限时和压力的环境下检验自己的算法思维是否真的内化了。5.4 一个提高效率的小习惯最后分享一个我个人的小习惯每道题AC之后在本地代码文件开头写三行注释分别是题目编号、核心思路关键词、复杂度。比如# 224 基本计算器栈处理括号符号翻转O(n) # 875 爱吃香蕉的狒狒二分答案向上取整O(n log m)这个习惯听起来很土但坚持下来之后你会发现复习的时候效率翻倍。不用重新读一遍题不用再想一遍思路扫一眼注释就能快速回忆起这道题的全部要点。热题100一共100道题三轮刷下来你的本地笔记就是一份完全属于你自己的算法面试复习资料比任何题解书都管用。我第二轮刷到一半的时候曾经一度想放弃觉得“背题”有什么意义。但坚持把每道题吃透把复杂度算清楚把边界条件捋顺之后再回头看第一遍刷题的自己才明白差距其实不在“做了多少题”而在“想透了多少题”。热题100第二遍值得你慢一点。
返回列表