LeetCode Top 100高频题刷题指南:吃透双指针、BFS与动态规划
还记得我第一次打开LeetCode的Top 100题单时第一反应是这些题真的够用吗刷完到底要花多久说实话很多帖子喜欢把这份题单捧成“面试通关秘笈”但我完整刷过两轮之后更愿意把它看作一份高频考点的最小覆盖集。你不需要用题海战术把自己淹没但Top 100里确实藏着面试官最爱考的那些套路双指针、滑动窗口、BFS/DFS、动态规划、二分答案、单调栈……只要吃透这些题背后的思维模型再去碰那些没见过的题你会发现大部分都是老朋友换了层皮。这篇内容不是简单罗列题号我会把Top 100里最有代表性的几类题目拆开揉碎讲清楚为什么这么解、踩过哪些坑、怎么从“看懂题解”变成“自己会写”。不管你是刚开始刷题的转码选手还是已经刷了一两百道但总觉得没开窍的进阶者这篇文章都适合拿来当一份带避坑指南的参考。1. 聊聊这份Top 100清单的含金量1.1 它到底解决了什么问题LeetCode题库现在有三千多道题没人能靠蛮力刷完。Top 100的出现本质上是帮你解决了一个信息筛选问题哪些题最值得花时间。这个榜单不是随便排的它综合了出现频率、题目难度、考点覆盖三个维度。你会发现很多题号的影子反复出现在不同公司的笔试题里比如两数之和、三数之和、最长回文子串、合并区间、二叉树层序遍历这些题目几乎成了面试标配。有人觉得Top 100太简单都是基础题有人觉得太难第一题就卡住。这两种感受其实都对因为Top 100的难度跨度本来就很大。它的价值不在于每一道题都难到让你怀疑人生而在于它把“大多数面试中会用到的算法思想”做了一个浓缩。你刷完这100道相当于把数组、链表、树、图、动态规划、回溯、贪心这些大模块都过了一遍而且是在有限的题目数量内完成的。我的建议是别把Top 100当成“刷完就万事大吉”的终点而是当成“建立算法思维框架”的起点。框架搭好了后面的扩展刷题才有意义。1.2 适合谁刷刷到什么程度才算过关不同基础的人刷Top 100的策略完全不同。如果你是零基础转码不要一上来就按题号顺序硬刷。建议先按专题刷比如先把双指针相关的几道题放在一起搞定再集中刷二叉树。这样你每次只在学习一种新套路脑子不容易乱。如果你已经刷过一些题但总觉得面试时一紧张就写不出来问题往往出在“看过题解”而不是“真正会做”。这时候你需要的是二刷甚至三刷重点是合上题解、白板手写、讲给自己听。那刷到什么程度算过关我给自己定的标准有三条第一不看题解能写出最优解至少能写出一个正确解第二能说清楚为什么用这个数据结构、这个算法复杂度是多少第三能给题目换个条件你说得出来解法会不会变。这三条做到了才叫真正掌握了一道题而不是“见过”。2. Top 100高频考点的底层逻辑2.1 数据结构维度从数组到树层层递进Top 100里数据结构大概分布在这样几个层级数组和字符串是最基础的操作对象它们本身不复杂但配合双指针、前缀和、滑动窗口这些技巧后就能玩出很多花样。链表是很多人的薄弱点因为它涉及大量指针操作和边界判断比如反转链表、环形链表、合并K个有序链表这些题表面上是考链表实际上考的是你对指针状态变化的掌控力。树是Top 100的重头戏尤其是二叉树。前序、中序、后序、层序每一种遍历都是一个套路。但更关键的是很多题看起来不是树实际上要用树的思维去解比如用递归做括号生成、用DFS做岛屿数量。图的比重虽然不如树但图论里的BFS/DFS思想几乎渗透在所有涉及“状态扩散”的题目里腐烂的橘子就是典型代表。栈和队列、哈希表、堆这些结构看起来不起眼但它们在Top 100里的出场率极高。哈希表把查找时间从O(n)降到O(1)这是无数题解优化的核心手段单调栈解决“下一个更大元素”这类问题优先队列在很多贪心和Top K问题里是标配。掌握数据结构本身只是第一步能根据题目场景选出合适的数据结构才是刷题真正的门槛。2.2 算法思想维度五大常用思想占据大半江山Top 100里算法思想可以浓缩成五个关键词双指针、动态规划、BFS/DFS、二分查找、贪心。双指针更多作用于有序数组和链表它的核心是让两个指针按照某种策略移动把暴力枚举的O(n²)降下来。动态规划考的是状态定义和状态转移方程Top 100里的动态规划题相对友好但从中级到高级的跳跃感很强。BFS/DFS这对兄弟一个适合求最短路径、逐层扩散一个适合穷举所有路径、回溯所有可能。它们经常和树、图一起出现但也能和二维矩阵绑定比如岛屿类题目和腐烂的橘子。二分查找的难点在于识别“这道题可以二分”LeetCode热门题里那几道吃香蕉、运货船、分割数组的题目考的就是这种“答案域二分”的思维。贪心算法在Top 100里题目不多但每一道都很有代表性比如跳跃游戏、买卖股票的最佳时机。贪心的核心是证明局部最优能推出全局最优面试时你不需要严格证明但要能解释清楚为什么这一步的贪心选择是安全的。2.3 思维模型维度遇到新题怎么和旧题类比刷题刷到后面你会发现自己进入了一个“套模型”的阶段。看到“最值子区间”想到滑动窗口看到“是否可行”想到二分答案看到“连通区域扩散”想到BFS/DFS看到“子序列不求连续”想到动态规划。这种联想能力才是Top 100训练出来的真正内功。怎么训练这种联想我的做法是给每道题打标签。比如“盛最多水的容器”标签是数组、双指针、贪心“合并区间”标签是排序、扫描线“爬楼梯”标签是一维DP、斐波那契。打完标签之后你会发现很多题其实在共享同一套底层代码。理解到这个层面刷题才不是孤零零地记题解而是在积累思维模型。3. 四道高频题目的逐层拆解3.1 994 腐烂的橘子多源BFS的经典起手式这道题可以说是BFS题里最值得反复做的一道。它的场景很直观一个二维网格里0是空格、1是好橘子、2是烂橘子每分钟烂橘子会让上下左右的新鲜橘子腐烂要求返回所有橘子腐烂的最少分钟数或者返回-1表示有无解。很多人第一次做会陷入“每分钟去遍历所有格子找新感染的橘子”的思路每轮都对整个网格做O(mn)扫描如果总共要扩散k轮复杂度就是O(k*mn)网格稍大就超时。正确的打开方式是多源BFS把所有初始腐烂的橘子一次性全部丢进队列然后一层一层往外扩散。第一次感染的一批是第1分钟第二次感染的是第2分钟这就是“一分钟传染一次”的天然映射。代码框架大概是这个样子from collections import deque def orangesRotting(grid): m, n len(grid), len(grid[0]) q deque() fresh 0 for i in range(m): for j in range(n): if grid[i][j] 2: q.append((i, j)) elif grid[i][j] 1: fresh 1 minutes -1 dirs [(0,1), (0,-1), (1,0), (-1,0)] while q: minutes 1 for _ in range(len(q)): x, y q.popleft() for dx, dy in dirs: nx, ny x dx, y dy if 0 nx m and 0 ny n and grid[nx][ny] 1: grid[nx][ny] 2 fresh - 1 q.append((nx, ny)) return -1 if fresh 0 else max(minutes, 0)注意这里有个非常容易踩的坑minutes初始值是-1是等队列里元素开始变空时才累加。为什么不能从0开始因为如果一开始就没有腐烂橘子但也没有新鲜橘子整个网格本来就是空的正确结果应该是0分钟而不是-1或0之外的值。你只要在纸上拿一个只有0的网格跑一遍就明白了。还有要先统计新鲜橘子数量最后如果还有剩余说明有无解的情况直接返回-1。这道题的进阶是反过来做给你一个可感染全图的初始点求需要选哪些点作为源头。思路一样但需要把“源头集合”的选取逻辑加进去核心还是多源BFS。3.2 224 基本计算器表达式解析与栈的配合基本计算器系列224、227、772是LeetCode里很经典的“表达式解析”题目考察的是栈的运用和状态保存。题目要求实现一个带有加减乘除和括号的表达式计算器没有乘除的版本相对简单但有乘除和括号后难度直接上一个台阶。核心思路是用两个栈一个数字栈一个操作符栈。扫描表达式时遇到数字就连续读入整个数字遇到操作符要考虑和栈顶操作符的优先级关系遇到左括号直接入操作符栈遇到右括号则一直弹出操作符栈直到遇到左括号弹出过程中每弹出一个操作符就取出两个数字做一次计算结果再压回数字栈。处理单目负号是最容易翻车的地方比如“-21”这种表达式。当负号出现在表达式开头或者出现在左括号后面时它并不是减号而是负号。常见的处理技巧是在这种位置往数字栈里补一个0把负号当作“0减去这个数”来处理。这个技巧非常实用能处理一票边界Case。这道题真正的难点不在于语法规则而在于你对栈内状态的无误追踪。每次遇到右括号时你弹出的操作符数量是有限但不好预判的必须用while循环把所有括号内的操作都处理完。很多同学代码写到一半就乱就是因为在心里没有模拟好“当遇到右括号时括号内的操作符全部要计算干净”这个步骤。掌握这道题的栈处理逻辑后像逆波兰表达式求值、简化路径、字符串解码这些题都会顺势变得简单它们本质上都是“状态入栈、遇到特定字符出栈计算”的变体。3.3 875 爱吃香蕉的狒狒二分答案的思维转变这道题在LeetCode上有好几个版本比如爱吃香蕉的狒狒、运输货物、分割数组等它们都有一个共同特征题目给了一个目标约束要求找一个最小的可行值。我见过太多人看到这道题的第一反应是线性枚举速度从1开始一个一个试结果在数据范围大一点时直接超时。真正高效的解法是在答案域上做二分。这里的“答案”不是数组的下标而是狒狒的吃香蕉速度它的可能区间是1到最大堆香蕉数。我们不需要真的算出每个速度的结果只要在区间里不断用中间值去验证以这个速度能不能在H小时内吃完所有香蕉。能吃完就把右边界缩小吃不完就把左边界放大。验证一个速度是否可行的逻辑不复杂对每一堆香蕉吃掉它需要的时间是ceil(pile / speed)也就是(pile speed - 1) // speed。def can_finish(piles, speed, h): total 0 for pile in piles: total (pile speed - 1) // speed return total h def minEatingSpeed(piles, h): low, high 1, max(piles) while low high: mid (low high) // 2 if can_finish(piles, mid, h): high mid else: low mid 1 return low这道题最值得学习的不是二分模板而是识别“答案可二分”的信号题目要求最小可行速度、最短天数、最小运载能力且存在一个明显单调性——速度越大越有可能完成。你只要看到“求最小可行值”“最大最小化”这类表达就应该条件反射地想到二分答案。但这里有一个隐形的坑H的范围。如果H非常大比总香蕉数还大那么速度可能低到1就行这种时候你的二分区间下界应该从1开始但有些题目香蕉堆里可能都是很小的数下界从1没问题可是如果H小到和堆数相等你只能把每一堆香蕉单独花一小时吃完那速度最大就是max(piles)。边界想清楚代码就很难写错。3.4 239 滑动窗口最大值单调队列的真正价值这道题在Top 100里属于“看起来简单、做起来要命”的类型。暴力解法的复杂度是O(nk)窗口每滑动一次就重新扫描一遍窗口内的k个元素LeetCode的数据范围下直接超时。一个看似聪明的做法是用一个大顶堆维护窗口内元素堆顶就是最大值滑动窗口时删除离开的元素、加入新元素复杂度O(n log k)能过但不算最优。最优解是用单调双端队列时间复杂度可以降到O(n)。队列里存放的是数组的下标同时保证这些下标对应的值从队首到队尾是单调递减的。当窗口滑动时队首如果在窗口范围之外就弹出新元素进来之前把队尾所有不大于它的值都弹出因为它们在新窗口内不可能再成为最大值了。这样一来队首永远就是当前窗口的最大值。这个“从队尾弹出较小值”的操作本质上是把所有不可能再成为窗口最大值的元素提前淘汰掉。很多人不理解为什么要保留“次大值”原因在于窗口在右移队首的较大值一旦滑出窗口队尾的次大值就顶上来成为最大值。如果你只维护一个全局最大值而没有维护次大值窗口一滑就不知所措。这道题背后的单调队列思想还可以延伸到“滑动窗口内小于某个阈值的数量”“求窗口内最小值”等变题练熟这一道题比做十道同类型题目都有效。4. 刷题路线与方法论4.1 三轮刷题法广度优先、深度优先、随机抽取我刷Top 100用的是三轮刷题法。第一轮按专题刷每3到5天只刷一个主题比如这周刷数组双指针下周刷二叉树。第一轮的目的不是把每道题都做到最优解而是快速建立“这类题大概长什么样”的感觉。碰到完全不会的题不要死磕太长时间看题解、吃透思路、自己重写一遍然后在笔记里记下三个点核心思想、时间复杂度、自己卡住的地方。第二轮开始按题号顺序或重新从第一题开始这次关掉题解遇到不会的先独立思考至少20分钟。这一轮的产出是你自己的解题模板比如我把BFS的模板缩成四步初始化队列、初始状态入队、while循环层序遍历、判断终止条件。把每道题套进模板里能大幅提高写题速度。第三轮是随机抽题模式用随机数从100道题里抽几道来做模拟面试的手感。这轮最关键因为它能打破你按专题刷题时养成的“路径依赖”。很多人平时刷题按专题很顺一到面试就懵就是因为面试不会告诉你这道题考的是动态规划还是滑动窗口你必须自己判断。4.2 错题本与模板沉淀的做法错题本不是把错题抄一遍而是记录“我为什么卡住”。我见过很多人的错题本写得像题解备份等于白做。真正有效的错题本应该包含四个部分题目和标签、我的错误解法、错误原因是边界条件模型识别失败还是复杂度分析错了、正确解法的核心步骤。下次复习时先看标签和错误原因再看正确思路最后合上本子自己手写。模板沉淀这块我建议把常见的代码骨架整理成自己的风格。比如二分查找的边界条件有人用左闭右闭有人用左闭右开没有绝对的对错但你最好固定一种风格。BFS模板、DFS递归模板、滑动窗口模板、单调栈模板每个模板都用自己能记住的语言写一版附上来源题号。后期刷题碰到相似结构直接调用模板能节省大量时间。这里有个很实用的建议每周花半小时复习上周的模板和错题比每天刷新题更重要。算法思维本质上是记忆加应用缺乏复习刷过的题目两星期后基本就还给题解了。4.3 从“会做”到“会讲”的最后一公里面试时最尴尬的场景不是不会做而是做出来了但讲不清楚思路。很多同学在代码里加了一堆注释但面试官问你“为什么这个二分终条件是low high而不是”一下就卡住了。所以平时刷题把每道题都当成一次模拟面试写完代码后在脑海里用一两句话概括核心思路再把关键边界条件讲出来。比如腐烂的橘子你可以这样概括“这是一个多源BFS初始把所有腐烂橘子入队每层代表一分钟腐烂扩散时同时处理所有源点。最后统计是否还有新鲜橘子以此判断是否有解否则返回最大层数减一。”面试官听完这几个点就知道你是真懂而不是背题。同时你要训练“给题目改变条件后的应变能力”拿一道你做过的题问自己如果矩阵从grid变成稀疏矩阵怎么处理如果腐烂扩散速度变为每轮两格呢面试大多数时候不是考原题而是考你对原题的变形理解。能灵活应变说明你已经把这道题内化成了自己的思维工具。5. 刷题过程中的常见坑与排查实战5.1 超时不是玄学是复杂度没有算清楚LeetCode提交显示超时的时候很多人第一反应是“优化一下循环”但如果不做复杂度分析优化就是在打地鼠。超时通常有几种原因一是算法本身就是暴力复杂度比如滑动窗口最大值你用了O(nk)的解法二是代码在某个隐蔽的地方引入了额外循环比如在while循环里重复调用len()或者列表切片三是使用了错误的容器比如频繁在列表头部插入元素。排查思路是先在纸上写出每个循环的复杂度找到最高的量级再针对它做优化。多数Top 100题目的预期复杂度在题目的数据范围里是可以推算出来的比如n到10^5时O(n²)大概率危险O(n log n)基本安全。如果你发现自己的解法复杂度明显高于预期那不是代码写得不够优雅而是解题思路方向就错了这时应该考虑换数据结构和算法而不是继续微调。5.2 边界条件漏判是错误率最高的来源Top 100里边界条件的坑之多简直可以单独成书。数组为空、链表只有一个节点、树的根节点为空、窗口大小为1、目标值不在数组中、输入包含负数和空格、矩阵只有一行……这些Case每一条都能让人悔恨不已。我自己的教训是写得快不如想得全动手写代码之前先在题目的例子上把边界Case在草稿纸上过一遍。有个很笨但很有效的办法在代码里凡是取数组下标、取链表节点、判断递归出口的地方都停下来问自己一句“这里如果是空/越界/等于0会发生什么”。这种方法能筛掉大部分边界问题。还有就是要善用“防御性编程”在进入主要逻辑前先处理最容易炸的特殊场景比如数组为空直接返回0链表为空直接返回None。虽然会多写几行代码但能让代码鲁棒很多。5.3 死记硬背模板导致的假性会做很多人在刷题时会陷入“模板背得很熟但换道题就不会”的假性会做状态。模板代码本身没有错错的是使用模板时没有理解它的设计动机。比如二分查找模板里为什么用mid low (high - low) // 2而不是(low high) // 2因为前者规避了整数溢出风险为什么有些模板的循环条件是low high有些是low high因为这取决于你不会用什么方式排除搜索范围。遇到这种情况最好的解决办法是把模板当作一个起点每次用的时候都手动推导一遍。拿滑动窗口模板举例你每次写左右指针移动的时候都想一遍“左指针什么时候移动右指针什么时候移动窗口条件什么时候不满足”养成推导习惯之后即使面试现场突然想不起模板也能按照逻辑把代码写出来。5.4 心态与节奏刷题不是感动自己最后想聊聊比代码更重要的东西节奏。我很反对每天强迫自己刷五六道新题的做法因为大脑需要时间消化。我自己比较稳定的节奏是工作日每天两道一道新题加一道旧题周末集中三小时复盘一周的错题和模板而不是刷新题。刷题是一个漫长的积累过程短期突击的效果在面试中往往经不起深挖。另一个常见心理陷阱是“只刷简单题”或者“只刷难题”。只刷简单题会让你产生一种虚假的熟练感一旦面试遇到中等偏难题就露馅只刷难题则容易让人受到打击很难坚持。合理的方式是在专题阶段可以有意识地消灭掉该专题里的简单和中等题目在随机抽取阶段再去面对部分难题。如果在某一类题目上连续卡了好几道比如动态规划怎么都理不清状态方程先不要硬刚回头去把递推的基础题重新写一遍或者找两三篇讲这类题透彻的题解仔细读一遍。算法思维的突破往往不是做更多题而是在某个瞬间突然理解了底层逻辑。那一瞬间之后你再回头看之前觉得难的题会觉得异常清爽。我个人刷完Top 100两轮之后最大的体会是这100道题不是终点而是一把打开算法思维大门的钥匙。当你习惯了用双指针的视角去看数组用BFS的视角去看感染扩散用二分的视角去看求最小可行值你会发现题目之间那些隐藏的联系正在被你一点点串起来。这份通感才是刷题这件事真正迷人的地方。