ARTICLE DETAIL

资讯详情

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

LeetCode第21-40题算法模式解析:从二叉树递归到二分答案的面试指南

LeetCode第21-40题算法模式解析:从二叉树递归到二分答案的面试指南 刷LeetCode的Top 100面试高频题很多人真正开始掉队的地方就是第21到40题这一段。前20题还在Easy阶段“热身”到了这里Medium题一下子密集起来二叉树递归、滑窗、双指针、二分答案、表达式求值……混合着来光是看看题号就有点头皮发麻。再加上最近LeetCode周赛430刚结束讨论区里常被问到的“基本计算器”和“爱吃香蕉的狒狒”这类题目其实解题套路也都在这个区间里反复出现。这篇东西不是帮你逐题贴代码而是把这20道题背后的模式、边界坑、复杂度计算和面试讲题话术一次性讲透适合刚刷完前20题、正在摸爬滚打的选手也适合刷了好几遍却还是看到原题就卡壳的老朋友。1. 第21-40题在Top 100里的位置与整体设计思路1.1 为什么这一段是面试分水岭Top 100的榜单设计其实很有讲究。前20题基本是“开门红”帮你建立信心链表翻转、二分查找、简单的动态规划属于热身。但第21到40题难度曲线陡增题目的问法开始从“我会不会这个知识点”变成“我能不能在约束下把多个知识点揉在一起”。这一段的题目我翻来覆去做的感受是80%的题都能归到几个固定的算法框架里。有人觉得难不是因为算法本身难而是因为没意识到自己是在被考察“模式识别”的能力。举几个我印象特别深的方向二叉树类题目开始要求你定义递归返回值到底表示什么比如“路径和”“最大深度”“最近公共祖先”每种问法的递归状态设计都不一样。字符串相关的滑动窗口题目开始玩“窗口内状态怎么维护”不是单纯套个模板就能过。二分查找开始跳出“排序数组找数”变成“二分答案”你得会写check函数。表达式求值和模拟计算器这类题别看它只是“按部就班遍历”实际是考察栈的使用和边界处理能力面试里出现频率高得吓人。所以这一段是分水岭不是靠背题能过的。它逼着你从“知道有滑动窗口这个东西”进化到“能自己推导窗口收缩条件”。1.2 高频背后的算法模式图谱我习惯把第21-40题涉及的套路画成一张图来看不去记单个题号而是记模式。整理下来这一段最常出现的算法模式大概有下面这些模式典型特征面试偏好度二叉树递归/DFS问最大路径、直径、最近公共祖先极高双指针数组有序或退化成找三元组高滑动窗口子串、子数组、连续区间高二分答案求解“最小速度/最大容量/最短时间”中高栈模拟表达式求值、括号匹配高动态规划背包变形、一维DP、区间DP高前缀和哈希子数组和等于目标值中当你看到题面里出现“连续”“子数组”“不超过某个值”这类词第一反应就应该是滑窗或前缀和出现“问最大值的最小值”“最少几小时完成”这类词第一反应就应该是二分答案出现二叉树和路径第一反应就应该是递归状态设计。1.3 刷题目标设定与心态管理我给自己定的目标不是“刷完”而是“每种模式能独立写出一次满分答案”。20道题刷完如果你能脱口而出这七种模式的模板和复杂度说明这段就算是过去了。很多人在这一段崩溃是因为每天打开题单看到新题就慌觉得自己积累不够。我后来换了个思路每道题先花10分钟思考写出暴力解再尝试画状态图。能写出暴力解就算今天及格优化部分留到二刷。不要强迫自己一眼看穿最优解那样除了让你怀疑自己没有任何作用。2. 核心细节解析与实操要点2.1 树上问题的递归套路以二叉树最大路径和为例第21到40题里二叉树相关题目特别容易扎堆出现尤其是“路径”这一类的。比如二叉树中的最大路径和问的是从任意节点到任意节点的路径使得路径上节点值之和最大。这题我第一次写的时候只维护了一个全局最大值递归函数返回的是“包含当前节点和一边子树的最大路径”结果发现样例都过不去。后来才明白这里的关键是要分清楚两个角色递归函数的返回值必须是一个“单边贡献”也就是从当前节点往下走到某个叶子能形成的最大单路径和这样父节点才能拿它来拼接。全局变量负责收集“当前节点作为路径拐点”时的完整路径和也就是左贡献加右贡献加自身。换句话说递归函数返回“我能给爸爸什么”全局变量记录“我在自己家里怎么把左右孩子拼起来”。理解了这个二叉树里超过一半的递归题都能照猫画虎。这里有个很容易忽略的细节如果子树的贡献是负数你宁愿不要它因为含上负贡献会拉低总和。所以代码里对左右贡献要做一次max(0, 递归结果)的处理。这个处理不是一个小优化而是整个算法的正确性来源。复杂度是O(N)因为每个节点最多访问一次。面试时候被问到空间复杂度记得说是递归栈深度最坏情况是链状树O(N)。2.2 滑动窗口的通用模板最长子串类问题字符串类的滑窗题在这个区间里出现了不止一次。这类题看起来问法很多什么“无重复字符的最长子串”“最小覆盖子串”其实都是一个模板右指针往前探直到窗口不满足条件再收缩左指针在收缩过程中更新答案。我总结的模板是这样四步初始化两个指针左闭右闭或左闭右开看你习惯但一定要统一。右指针每移动一步更新窗口内的状态比如字符频次。判断当前窗口是否满足题目限制不满足就移动左指针并在移动时回退状态。每次窗口满足限制时尝试更新最终答案。前几天有个朋友问我为什么他写的滑动窗口老是死循环。我一看问题出在“窗口满足条件时收缩左指针”和“更新答案的位置”放反了。很多滑窗题的陷阱是答案应该在“收缩之后”或“收缩的同时”更新不能无脑在右指针移动后更新。还有个小技巧当窗口里只关心“某个值是否出现”而不是“出现多少次”时可以用一个计数器变量来记录有效字符种类数而不是每次重新数一遍哈希表。这个优化能把常数项降不少面试官看到这个细节一般都会点头。2.3 二分答案与上下界爱吃香蕉的狒狒最近热榜上“073爱吃香蕉的狒狒”特别多这个数字指的是LeetCode的一道经典题讲的是猴子吃香蕉要求在H小时内吃完求最小速度。这题就是典型的二分答案。在这里二分查找的对象不是数组下标而是速度值。你不需要去证明整个决策空间单调只需要判断如果速度快了能吃完速度再快一定也能吃完所以可以二分。写这类题最容易翻车的是check函数和二分边界的配合。我习惯把区间定义成“一定能吃”和“一定不能吃”分开用开区间写左边界设为1表示最慢速度。右边界设成香蕉堆里最大值因为速度比最大值还快没有任何意义。check函数里每堆香蕉吃完需要的天数是pile / speed向上取整也就是(pile speed - 1) // speed。向上取整这里很多初学者会写成直接浮点数除法再ceil拿去跑是能过的但面试时手写代码容易出现精度问题。用整数运算是最稳妥的。二分停止条件是左右边界相遇最后返回左边界即可。记住这类题在验证check时可以把每堆香蕉的耗时累加一旦超过题目给的H小时就可以提前跳出这是一个常数级优化在示例数据很大的时候体感很明显。2.4 模拟计算的策略基本计算器热词“基本计算器 leetcode”出来的时候底下评论基本分成两派一派觉得这种题就是“大模拟”背下来就行另一派觉得这种题没有套路全靠硬写。我现在的看法是它其实考察的是“状态机”的敏感度。基本计算器这类题一般要求你实现一个简单的加减乘除和括号字符串里有空格有括号可能还有负数。如果你真的去“从左到右算”很快就乱了。正确姿势是用一个栈把当前运算符、当前数字、当前结果分开维护。我的做法是这样碰到数字就把连续数字拼成一个完整的数。碰到运算符先把之前攒下的数字根据上一个运算符入栈或运算然后更新运算符。碰到左括号把当前结果和运算符压栈然后重置状态。碰到右括号弹出栈里的结果和运算符跟括号内的结果合并。这里最关键的是“什么时候处理数字”的时机。我见过的错误实现几乎都是因为读到数字就直接累加到结果里漏掉了符号和括号的影响。解决方案很简单只有遇到运算符或括号时才触发上一次的运算数字读到末尾时别忘了额外触发一次。另外一个坑是空格。很多人第一版代码没处理空格结果空格成了“字符串里的幽灵字符”。我建议在进入主循环前就把所有空格过滤掉或者在循环开头用if char 直接跳过。这两种方式体验差不多但过滤后逻辑更清晰。3. 实操过程与核心环节实现3.1 我的刷题时间表21-40题怎么排布我从第21题刷到第40题一共安排了大概10天。不是一天2题那种慢节奏而是一天分组刷3到4题组内全是同一个模式这样能形成肌肉记忆。天次模式主题推荐练习组合目标第1天二叉树递归路径和、最近公共祖先、树的直径掌握返回值与全局变量的分工第2天双指针三数之和、接雨水相关变体会证明指针移动的正确性第3天滑动窗口无重复最长子串、最小覆盖子串能独立写出窗口收缩条件第4天二分答案吃香蕉、分割数组会设计单调性check函数第5天栈与表达式基本计算器、括号生成能把状态机写顺第6天动态规划背包、编辑距离会画状态转移表第7天前缀和哈希和为K的子数组理解“哈希存前缀和的出现次数”剩下的3天用于二刷错题和乱序混合刷。我给自己的规则是每道题从读题到写出代码时间控制在45分钟以内超过就直接看题解但看完必须自己默写一遍。这个节奏不轻松但也不算自虐。第21-40题最忌讳的是“今天刷一道二叉树明天刷一道DP”这样每道题都是孤立的刷完就忘。按模式分组刷你会发现第二题往往是第一题的加壳版本。3.2 从想到写解题四步法与复杂度校验我在第21-40题区间吃到最大红利的一件事就是强迫自己用一套固定的解题流程而不是凭感觉硬写。这套流程四步走第一步读题后不急着写代码先用一个最小例子把题目用手推一遍。比如滑窗题拿一个长度只有3的字符串手动移动指针看看窗口状态具体怎么变化。第二步先写暴力解。很多人觉得面试里写暴力解会被嫌弃恰恰相反暴力解的价值在于帮你理解题目的约束在哪里。比如前缀和题暴力解法就是枚举所有子数组你会很快发现时间复杂度是O(N^2)然后自然地想到能不能用空间换时间。第三步分析暴力解的瓶颈。瓶颈里出现的“重复扫描”“重复计算”就是优化的线索。滑窗优化的是重复扫描动态规划优化的是重复子问题。第四步写优化解之前先算清楚时间复杂度和空间复杂度。比如二分答案是O(N log W)滑动窗口是O(N)二叉树递归是O(N)。把复杂度写在注释里面试官一看就觉得你是懂行的。这里我想特别强调第21-40题里很多题目如果你能做到“先写出能跑的暴力解再聊优化”就已经比我见过的大部分候选人强了。真正的问题不是写不出最优解而是连一个正确的基线都没有。3.3 面试表达环节30秒讲清思路讲思路是这部分最重要的能力。我的经验是不要一上来就讲最优解而是按这个顺序讲先明确题目的输入输出和边界条件比如“我先确认一下数组里有没有负数空数组怎么处理”。然后说“我先想到的是暴力解复杂度是O(N^2)因为每个子区间都要扫一遍”。接着指出“后来发现暴力解里有大量重复计算比如前缀和可以O(1)拿到区间和所以我改用哈希表”。最后说“这样时间复杂度降到了O(N)空间O(N)”。30秒讲完这个逻辑面试官基本能跟上你的思路。最怕的情况是你闷头就开始写滑动窗口写完才解释那对面早就跟丢了。在第21-40题这个区间里我还练了一个习惯每个算法都要能说出来“为什么这个解法是正确的”。比如滑动窗口为什么不会漏掉最长子串因为右指针每次往右扩展都覆盖了以该右端点结尾的所有可能窗口左指针收缩只发生在不满足条件时因此不会错过满足条件的更大解。这种证明不需要很数学但逻辑链条得闭环。4. 常见问题与排查技巧实录4.1 边界条件与指数级陷阱这个区间的题目边界条件真的能坑死人不偿命我把踩过的典型坑整理成了速查表常见问题症状原因解决办法二叉树递归返回错误示例过提交挂返回值设计成“全局路径和”确认递归函数返回值是单边贡献滑动窗口死循环运行超时左指针更新时忘记回退窗口状态在while循环内同步更新计数器和“有效种类数”二分答案陷入死循环结果永远不对区间边界收敛方式写错使用左闭右开区间或者用mid (left right 1) // 2处理靠左情况表达式求值负数结果错减号后数字丢失没有记录符号位用栈存结果和运算符遇到减号把当前数字取负入栈数组越界个别用例异常双指针右指针走到头每次移动右指针前检查越界递归爆栈系统栈溢出递归深度超过默认限制改用迭代或增大递归深度之前先评估栈深度边界条件不是背出来的是靠“手推小例子”练出来的。我刷第21-40题时每道题过完样例之后都会额外构造三组极端输入空输入、只有一个元素的输入、最大语义的输入。这三组输入能拦住80%的边界问题。4.2 递归爆栈与记忆化的取舍第21-40题区间里涉及DFS和二叉树递归的题目不少。在LeetCode上默认递归深度上限大约是1000如果正好碰到一条链状的树路径相关的题目很容易递归爆栈。我的排查思路是先观察题目给的数据量。如果树的节点数量超过1000递归解法就不是最优选择。但面试场景下你也可以先写递归版本然后主动跟面试官提一句“这个解法在树特别深的时候可能栈溢出我会改成迭代或者自底向上”。这种主动暴露风险的做法比被面试官追问后再支支吾吾要好得多。记忆化用在哪里也很关键。比如动态规划题有的人一看到递归就加记忆化结果发现空间复杂度爆炸。记住一个原则有没有重叠子问题决定该不该加记忆化如果递归树是线性的那记忆化就是多此一举。4.3 面试时翻车的瞬间手滑、卡壳、被追问怎么办刷题和面试是两回事我曾经在第21-40题区间里反复练习“被追问”的场景。最典型的一个追问就是面试官指着你的滑动窗口问“为什么你能保证这个答案是对的”如果这时候你愣住印象分会大打折扣。我的应对分三层第一层用一个小例子现场演示指针移动过程说明每一步都在“枚举右端点收缩左端点”不会漏掉解。第二层说清楚单调性比如窗口状态满足条件时左指针可以安全收缩因为继续扩展只会让窗口更不满足条件。第三层如果实在讲不清证明就诚实说“我先用暴力解和随机测试对比过还没发现反例”也比支支吾吾强。另一个翻车点是手滑写错变量名。这种问题没法完全避免但有办法降低概率写代码的时候把左指针叫left右指针叫right不要用i、j。看起来是小事但能在你紧张的时候帮你节省大量的调试时间。4.4 二刷三刷的策略什么时候该重做这道题我现在的习惯是第一遍刷完后把错题和有思路但没写全的题标记一下。第二遍只刷标记的题但要求自己不看任何题解从零在白板上写。第三遍我挑那些当时花了一晚上才看懂的题用文字向一个虚拟读者讲解整个思路。这里有个让我很意外的现象有些题我第一遍独立写出来了但第二遍再写居然卡住。原因是第一遍我记住了别人的解法但没内化。于是我给自己的规则是每道题的第一遍至少要有10分钟的独立思考时间。如果10分钟内完全没头绪才允许看题解。这个规则执行下来第21-40题二刷的通过率比一刷高了非常多因为“看题解”从被动接收变成了主动校验。根据我个人刷这段题的实际体会第21到40题最值得花时间的不是把每道题的代码背下来而是把递归返回值设计、滑动窗口收缩逻辑、二分答案的check函数这三个基本功彻底揉碎。这三板斧顺了后面那些所谓压轴题说白了都是在这几个框架上加点状态和约束条件。最后再分享一个小技巧每次刷完一组同模式的题用一张纸把模板和复杂度默写下来贴在电脑边上。再看到新题时先对照纸上的模式做映射而不是直接冲进题解区。这招看着笨但实测下来比刷五遍题都管用。
返回列表