ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

二叉树刷题指南:从递归遍历到Hot 100经典题型的核心模板

2026/9/11 15:06:42 拓冰建站 浏览量
二叉树刷题指南:从递归遍历到Hot 100经典题型的核心模板 打开力扣的二叉树标签再把Hot 100翻一遍你会发现一个规律二叉树这十几道题几乎就是算法面试的“必考保留节目”。Hot 100里的二叉树题目从最大深度、翻转二叉树到路径总和、最近公共祖先表面上看是不同题型实际上都围绕几个核心思维模型打转。我的感受是二叉树是训练递归思维最好的训练场因为你只要把“递”和“归”想明白了很多题就是在同一个模板上改两三行。这篇文章不是带你一道题一道题地抄题解而是把我刷完Hot 100二叉树部分之后总结出来的题型框架、代码模板、常见误区和调试技巧一次性聊透。无论你是刚开始刷题的新手还是已经被递归搞得头晕的老手读完应该都能对二叉树这一块建立起一个清晰的地图。回想我自己刷题的经历二叉树这部分可以说是“痛苦中带着爽感”。痛苦在于递归的调用栈有时候真的绕不明白爽感在于一旦想通了某个模式LeetCode 上相关的题会一次性解锁好几道。尤其是Hot 100这个题单它把二叉树题目从易到难排得非常讲究非常适合用来建立体系化认知。我整理这篇文章就是想帮你把这种“体系感”提炼出来。1. 为什么二叉树是Hot 100里的“兵家必争之地”1.1 二叉树题型的现实地位与考察逻辑Hot 100这100道题覆盖了数组、链表、动态规划、图论等大量方向但二叉树题型的数量稳定占据前十的位置。这不只是因为二叉树题目本身好出题、好变型更是因为二叉树这种数据结构天然适合考察几个核心能力递归的理解深度、指针/引用操作的熟练度、对问题规模的分解能力。你去看大厂面试题二叉树也是出现频率最高的数据结构之一。原因很简单树的结构足够简单但逻辑可以无限变复杂。一个TreeNode定义就三行class TreeNode: def __init__(self, val0, leftNone, rightNone): self.val val self.left left self.right right就这三行能演化出遍历、搜索、构建、序列化、最近公共祖先等几十种考法。面试官不需要准备特别复杂的输入随便画一棵树就能把候选人的代码能力问出个大概。所以Hot 100选中这么多二叉树题完全是对面试风向的精准把握。1.2 我在刷题中总结出的二叉树“三边关系”二叉树题目看似零散但互相之间关系极强。我把它们分为三类一边是“遍历类”包括前序、中序、后序、层序遍历这是所有树题的基础一边是“构建类”比如通过前序和中序还原二叉树、将有序数组转换为二叉搜索树这类题考的是对遍历性质的理解还有一边是“路径与递归类”比如求最大深度、路径总和、二叉树的直径这类题看起来是应用题本质还是递归过程中传递和收集信息。理解了这个三边关系之后你再去刷Hot 100里的二叉树题就不会觉得是一道道孤立的题而是在围绕几个核心知识点反复变招。先跑通遍历再搞定构建最后啃路径和递归整个板块就比较稳了。下面我会按照这个逻辑把具体的解题模式和代码框架拆开来讲。2. 遍历是二叉树的第一性原理2.1 递归遍历的统一写法三步法二叉树的所有操作本质上都建立在遍历之上。Hot 100里很多难题拆到底层都是在某个遍历过程中做状态收集。所以遍历代码必须达到“闭着眼睛都能写”的程度。递归遍历有一个非常固定的三步法模板以中序遍历为例def inorder(root): if not root: return inorder(root.left) # 左 visit(root.val) # 根 inorder(root.right) # 右这个模板的精髓在于先处理终止条件再定义单层递归做什么。前序就是把visit移到最前面后序就是把visit移到最后面。很多新手写递归容易陷入“试图理解每一层调用栈”的陷阱其实完全没必要。你要做的是相信这个函数已经完成了它定义好的功能然后按照左中右的位置调用它。这种“信任递归”的思维方式是二叉树上递归题的核心心法。我记得自己最开始刷二叉树时总喜欢把递归展开成一层一层的调用栈去模拟结果越模拟越晕。后来跟着题解做多了才发现真正高效的思考方式只有一句话假设这个递归函数已经能返回我想要的结果那么当前这一层应该怎么调用它这句话想通了代码自然就写出来了。2.2 三种遍历顺序与题目类型之间的对应关系遍历顺序不只是代码顺序它直接决定了你能在遍历过程中拿到什么信息。前序遍历根 - 左 - 右天然适合做“从上往下”的构建和复制。比如镜像二叉树、翻转二叉树都是前序思路先处理当前节点再递归处理左右子树。中序遍历左 - 根 - 右天然带顺序属性。因为二叉搜索树的中序遍历结果是严格递增的验证BST、求第k个最小元素这类题目本质就是在吃中序序列的红利。你写的第一行核心逻辑可能就是def inorder(root): if not root: return inorder(root.left) # 在这里检查当前值是否比上一个值大 inorder(root.right)后序遍历左 - 右 - 根天然适合收集“从下往上”的信息。二叉树的直径、最近公共祖先、求子树和这些题都需要左右子树先把结果算出来才能计算当前节点。我自己的经验是不确定该用哪种遍历时先想清楚“当前节点的答案能不能由左右子树推导出来”如果能八九不离十就是后序遍历。2.3 层序遍历的迭代模板与应用扩展层序遍历在Hot 100里出现的频率也很高比如二叉树的层序遍历、二叉树的右视图、填充每个节点的下一个右侧节点指针。层序遍历的迭代模板也是固定的from collections import deque def levelOrder(root): if not root: return [] res [] q deque([root]) while q: size len(q) level [] for _ in range(size): node q.popleft() level.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) res.append(level) return res这里的size len(q)很关键它锁定了“当前层”的节点数量避免把下一层的节点混入当前层。右视图那道题就是在每一层取最后一个节点填充右侧指针的题就是在层序遍历时记住上一个节点然后把指针连上去。可以说层序遍历就是一把万能钥匙很多题在递归不好写的时候用层序迭代反而更直观。3. Hot 100二叉树题型的核心解题模式拆解3.1 最大深度与直径后序遍历收集左右信息二叉树的最大深度可以说是Hot 100里最基础的二叉树题之一。它的解法很多DFS递归最简洁def maxDepth(root): if not root: return 0 return max(maxDepth(root.left), maxDepth(root.right)) 1这行代码背后就是后序思想左右子树的深度分别算出来最大值加1就是当前节点的最大深度。注意这里的加1发生在递归返回的路上这就是后序遍历先拿到子树的答案然后再处理当前节点。二叉树直径这道题是最大深度的升级版。直径的定义是任意两节点之间路径长度中的最大值。路径长度等于左子树深度加右子树深度。但要注意最长的路径不一定经过根节点它可能藏在某个子树的内部。所以需要在递归过程中用一个全局变量不断更新最大值class Solution: def diameterOfBinaryTree(self, root): self.max_d 0 def depth(node): if not node: return 0 left depth(node.left) right depth(node.right) self.max_d max(self.max_d, left right) return max(left, right) 1 depth(root) return self.max_d这个模式的精髓在于递归函数返回的是“当前节点深度”但过程中顺便更新了“以当前节点为转折点的直径”。这种做法在二叉树题目里太常见了后面的“路径总和III”也是类似思路。可以先定义一个“主递归”再定义一个“辅助递归”在主递归里遍历所有节点在辅助递归里计算从当前节点出发的路径数量两个递归嵌套起来题目就解出来了。这种“递归套递归”的模式一开始可能觉得别扭但写多了就会觉得很自然。3.2 翻转二叉树与对称二叉树前序对比左右结构翻转二叉树这道题算得上是Hot 100里的“网红题”当年Homebrew作者因为没写出来还闹过新闻。它的解法非常直观也就是前序遍历先交换左右子树再递归翻转左右子树def invertTree(root): if not root: return None root.left, root.right root.right, root.left invertTree(root.left) invertTree(root.right) return root这里需要注意一个细节先交换再递归和先递归再交换效果上等价但思考方式不同。前序版本更符合“从根节点开始处理”的习惯很多人在这个题上去纠结“到底该用前序还是后序”其实没必要只要你保证每个节点的左右子树都完成了交换顺序不影响结果。对称二叉树这道题就是翻转二叉树的反向应用。它的判断标准是左子树的左节点等于右子树的右节点左子树的右节点等于右子树的左节点。这里要注意递归函数接收的是一对节点不是单个节点def isSymmetric(root): def check(left, right): if not left and not right: return True if not left or not right: return False return (left.val right.val and check(left.left, right.right) and check(left.right, right.left)) return check(root.left, root.right)这个题的核心套路是“镜像对比”。两棵树互为镜像其实就是把一棵树按中轴线翻过来和另一棵树完全一致。只要能理解到这一点代码就是按照对比规则一路递归往下写。这种“双指针式递归”在二叉树题里也相当常见比如判断两棵树是否相同也是同一类思路。3.3 从前序与中序遍历构造二叉树靠区间切分还原结构热词里出现了“知道二叉树先序和中序确定树的样子”这是非常经典的一道题对应LeetCode 105题。它的难度不算特别大但很考验对遍历性质的理解。前序序列的第一个值一定是根节点找到根节点在中序序列中的位置后中序序列左边是左子树、右边是右子树接着根据“左子树节点数量”回头去前序序列里切分出左子树和右子树的范围然后递归构建。我第一次写这个题时最痛苦的地方是索引计算老是出错。后来我总结了一个比较稳妥的写法def buildTree(preorder, inorder): index {val: i for i, val in enumerate(inorder)} # 哈希表存中序索引 def helper(pre_left, pre_right, in_left, in_right): if pre_left pre_right: return None root_val preorder[pre_left] root TreeNode(root_val) in_root_idx index[root_val] left_size in_root_idx - in_left root.left helper(pre_left 1, pre_left left_size, in_left, in_root_idx - 1) root.right helper(pre_left left_size 1, pre_right, in_root_idx 1, in_right) return root return helper(0, len(preorder) - 1, 0, len(inorder) - 1)这里最关键的是left_size的计算。很多解法直接用中序的根节点位置去切前序序列但前序和中序的下标并不是一一对应的必须通过左子树节点数量来换算。用哈希表把中序序列的值到索引的映射存下来每次取根节点位置就是O(1)。整个算法的时间复杂度是O(n)空间复杂度是O(n)。如果不用哈希表每次都线性找根节点时间复杂度会退化成O(n^2)平时做题数据量小看不出来一到面试官追问复杂度就露馅了。3.4 验证二叉搜索树与最近公共祖先中序与后序的经典演出验证二叉搜索树是Hot 100里比较阴险的一道题。很多新手包括我自己第一次写都会写出这种错误解法只比较当前节点和左右孩子的大小。但实际上BST要求的是左子树所有节点都小于根节点而不仅仅是左孩子。反例也很简单一棵树左子树的右孩子可能比根节点还大这种情况光看局部是发现不了的。正确思路是利用中序遍历递增的性质或者每个节点传一个允许的取值范围。中序法的代码很简单def isValidBST(root): prev float(-inf) def inorder(node): nonlocal prev if not node: return True if not inorder(node.left): return False if node.val prev: return False prev node.val return inorder(node.right) return inorder(root)这段代码的精髓在于用全局变量prev记录上一个节点的值只要出现当前值小于等于上一个值就直接返回False。这个题的另一个陷阱是节点的初始值可能正好是负无穷所以判定条件必须用而不是否则会漏掉重复节点。我在面试中就被这个边界坑过一次后来学乖了所有涉及“严格递增”的判定都会先问自己等于算不算合法最近公共祖先就完全是后序遍历的身影了。思路是如果一个节点的左右子树分别包含p和q那这个节点就是最近公共祖先如果p或q本身是某个节点的祖先那在递归过程中会遇到“当前节点等于p或q”的情况直接返回当前节点即可。写法如下def lowestCommonAncestor(root, p, q): if not root or root p or root q: return root left lowestCommonAncestor(root.left, p, q) right lowestCommonAncestor(root.right, p, q) if left and right: return root return left if left else right你注意看当前节点先处理了“自己是p或q”的情况然后递归左右子树最后根据left和right的返回情况判断谁是公共祖先。这个过程特别像“下面传来消息我汇总之后做决定”是典型的后序思路。这个题还有一个版本是BST的最近公共祖先那个反而更简单只需要看p、q当前值与根节点的大小关系来决定往左走还是往右走根本用不到递归遍历整棵树。3.5 路径总和系列回溯与前缀和的配合Hot 100里的路径总和题目有两道一道是“路径总和III”要求路径必须从根节点出发到叶子节点一道是“路径总和III”要求路径不需要从根节点出发只需要方向向下即可。第一道相对简单递归往下传目标值遇到叶子判断是否相等就行。第二道如果不看题解自己硬想可能会走很多弯路。路径总和III的标准解法是“前缀和 回溯”。所谓前缀和就是从根节点到当前节点的路径上所有节点值之和。如果某个祖先节点的前缀和是sum_so_far - target说明从那个祖先的下一个节点到当前节点的路径和正好等于target。用一个哈希表记录当前路径上各个前缀和出现的次数就能在遍历过程中O(1)查出是否存在满足条件的路径组合。class Solution: def pathSum(self, root, targetSum): self.res 0 prefix_sum_count {0: 1} self.target targetSum def dfs(node, cur_sum): if not node: return cur_sum node.val # 查一下当前路径上有没有满足条件的前缀和 self.res prefix_sum_count.get(cur_sum - self.target, 0) prefix_sum_count[cur_sum] prefix_sum_count.get(cur_sum, 0) 1 dfs(node.left, cur_sum) dfs(node.right, cur_sum) # 回溯恢复现场 prefix_sum_count[cur_sum] - 1 dfs(root, 0) return self.res我在做这道题时踩过一个坑忘记在递归返回时对哈希表进行回溯。因为左右子树共享同一个哈希表如果不把当前节点的前缀和计数减回去右子树在查询时会把左子树的路径也算进去结果就是答案偏大。这种“同一路径上共享数据结构”的题回溯思想必须刻在DNA里。路径总和III这道题算是Hot 100二叉树部分比较综合的一道题它同时考察了DFS、前缀和、回溯这几个点如果这道题能一遍写对说明二叉树这部分的递归功力已经比较扎实了。4. 二叉树题目的通用难点与排查技巧4.1 递归栈溢出与深度过深问题二叉树在最坏情况下可能退化成链表比如一棵只有右子树的树深度等于节点数量。这种情况下递归的调用栈会非常深在面试或笔试里如果数据量到达几万层Python直接报RecursionErrorC则有可能爆栈。我在刷题时遇到过一道题用递归解法能通过但耗时很高换成迭代法后性能立刻上来了。所以对于“求深度”“判断平衡”“路径总和”这类题如果递归写腻了最好也掌握迭代写法。以最大深度为例迭代法可以用栈模拟后序也可以用层序def maxDepth(root): if not root: return 0 stack [(root, 1)] max_d 0 while stack: node, depth stack.pop() max_d max(max_d, depth) if node.left: stack.append((node.left, depth 1)) if node.right: stack.append((node.right, depth 1)) return max_d这种栈二元组的写法本质上就是在模拟递归过程中的状态。当递归的“隐式栈”可能溢出时换成显式栈就是最稳妥的兜底方案。我的建议是每道递归解法的题都顺手练练迭代写法不是为了炫技而是为了在面试中遇到“能不能不用递归”的追问时不慌。4.2 边界条件疏漏空树、单节点、负值二叉树题目最容易被扣分的不是算法本身而是边界条件处理不全。我自己总结了三个高频边界空树root is None时返回什么最大值返回0路径和返回False构建树返回None。这个只要在函数开头写一行判断就行但很容易忘。单节点递归函数里如果只写了左子树判断没有对称地处理右子树在单节点树上可能没问题但一旦树变成两个节点就会出错。负值路径总和系列中节点值可能为负初学者容易一看到当前和已经大于target就提前返回这在节点全为正时是对的但有负值时就会漏掉正确路径。正确做法是不要基于“当前和大于target”做剪枝除非题目明确说节点值全为正。我建议在写完二叉树代码后固定用三组测试用例检查空树、只有根节点、左右子树都存在的普通树。这三组跑通了大部分边界问题就能暴露出来。4.3 递归修改原树结构时的指针悬挂问题有一类题目要求“就地修改”二叉树比如把二叉树展开为链表、填充每个节点的右侧指针。这类题目最怕的就是指针悬挂也就是把节点的 left 或 right 改了之后后面再需要用到原来的子节点信息结果发现已经丢了。以展开为链表为例一个稳妥的做法是先右后左的后序遍历class Solution: def flatten(self, root): self.prev None def dfs(node): if not node: return dfs(node.right) dfs(node.left) node.right self.prev node.left None self.prev node dfs(root)这段代码的核心思路是让递归先处理最右边的节点然后通过self.prev记住已经处理好的链表部分当前节点只需要把自己的右指针指向self.prev即可。如果你用前序遍历先改右指针那么原来的右子树还没被处理就被覆盖掉了这棵树就废了。所以遇到“修改原树”的题目一定先问自己我改掉这个指针之后原来的子树还有没有其他地方能访问到如果没有就必须先处理那个子树。4.4 构造测试用例的土办法有时候自己写出来的代码在本地跑没问题一提交就超时或者报错这时候最需要的是构造一个能复现问题的最小用例。但很多新手不会构造测试用例只会拿题目的示例直接跑。我的土办法是先把一棵二叉树手动画出来然后用层序序列转成代码里的输入。比如想测试一棵三层的满二叉树输入就是[1,2,3,4,5,6,7]。这个序列按层序生成写起来特别快。如果测的是搜索树相关就专门构造几条链表式的退化树比如[1, None, 2, None, 3, None, 4]这种。我在本地调试时经常用LeetCode自带的控制台直接把数组输入进去看输出比自己手写构造树要高效太多。另外在LeetCode上做题时print打印中间变量这种土办法也是可以用的很多语言在调试模式下都能直接打印。不要觉得这样低级能帮你定位问题的手段就是好手段。5. Hot 100二叉树题单的学习路线与扩展思考5.1 建议的刷题顺序Hot 100里的二叉树题按学习顺序可以分为四档第一档是入门包括二叉树的最大深度、翻转二叉树、对称二叉树、二叉树的直径。这几道题主要练递归的理解和遍历模板。第二档是遍历进阶包括二叉树的层序遍历、二叉树的右视图、从前序与中序遍历构造二叉树。这几道题开始涉及迭代和构建。第三档是搜索与路径包括验证二叉搜索树、二叉搜索树中第K小的元素、路径总和III、二叉树的最近公共祖先。这几道题会混合前序、中序、后序、回溯等多种技巧。第四档是修改与序列化比如把二叉树展开为链表、序列化和反序列化二叉树。如果你能把这四档按顺序刷完Hot 100二叉树板块基本就通关了。而且你会发现这十几道题的通用代码模板其实不到五个翻来覆去都是那几个模式在变形。5.2 那些热词里提到的扩展概念热词里出现了“线索二叉树”。Hot 100本身不会直接考线索二叉树但面试里偶尔会问一句概念题。线索二叉树的核心思想是在空闲的指针域里存放前驱和后继节点信息以加速中序遍历。比如一个节点如果有左孩子它的左指针指向左孩子如果没有左孩子就把左指针指向中序遍历的前驱节点。这样在做中序遍历时就不需要递归或者栈了可以直接沿着线索走。这一概念的本质是“用空间换时间”理解它有助于加深对二叉树遍历顺序的敏感度但刷题阶段不用花太多精力。热词里还有一条“二叉树的深度”。这其实和最大深度是一回事但在剑指Offer里叫“二叉树的深度”在LeetCode里叫“Maximum Depth of Binary Tree”。名字不同解法完全一样。刷题时看到类似题目不要慌认清本质比记住题名重要。5.3 从二叉树延伸出去的面试考点二叉树掌握扎实之后可以顺势扩展去刷二叉搜索树相关的问题。二叉搜索树本质上就是一颗有序的二叉树在它上面做查找、插入、删除都能用递归或者迭代完成时间复杂度平均是O(log n)。Hot 100里这个方向的题目涉及将有序数组转换为二叉搜索树、验证二叉搜索树等考得还算基础。再往外延伸就是字典树Trie和堆。字典树可以用来做单词搜索堆本质上是完全二叉树常被用来实现优先队列。这些数据结构面试也很爱考而且都和二叉树有血缘关系。如果树的基础打得牢学这些会轻松很多。所以我把二叉树称作数据结构里“承上启下”的一环一点也不夸张。我个人在实际刷题中的体会是二叉树部分是所有数据结构里投入产出比最高的板块。你不一定要刷几百道树题只要把Hot 100里的这十几道题吃透把递归三步法、遍历顺序、回溯思想这些内化成一个思维习惯再去写其他类型的题会发现代码能力也有了肉眼可见的提升。遇到没见过的树题先稳住想清楚它的解属于哪一类模式是构建、是搜索、还是路径统计然后套模板、补细节基本就八九不离十了。希望这篇文章能帮你把二叉树这条路趟得更顺少踩几个我当年踩过的坑。