ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

二叉树最大深度怎么求?递归与迭代两种解法详解

二叉树最大深度怎么求?递归与迭代两种解法详解 1. 题意拆解104题到底在问什么1.1 深度这个“常识概念”的准确定义刷力扣的二叉树系列104题几乎是每个人绕不开的第一道门槛。它看起来简单——求一棵二叉树的最大深度但很多新手在这里摔的第一跤往往不是不会写代码而是对“深度”这个概念的理解停留在直觉层面。二叉树的深度标准定义是从根节点到最远叶子节点的最长路径上的节点数。注意这里说的是节点数不是边数。所以一棵只有一个根节点的树它的深度是1而不是0。空树null的深度是0。这两个边界值是整道题的基础也是后续所有递归和迭代写法的根基。我见过不少人在评论区和题解区问为什么我写的代码遇到空树就返回1答案几乎都是把空子树的返回值定义错了。空子树没有节点深度就是0返回1等于凭空多算了一层而根节点那层已经在“当前递归层”的返回逻辑里加过了。1.2 为什么这道题值得认真做这道题在力扣里属于“二叉树的遍历”和“力扣热题100”的双料常客。从难度上看它只是简单题但它的价值完全不在于“能AC”而在于它是一把尺子——量出你对递归、分治、层序遍历、树形结构这四件事的掌握程度。把这道题吃透你相当于同时想清楚了三件事二叉树天然适合用递归去描述因为它本身就是递归定义的结构树的深度和层数是同一枚硬币的两面迭代解法中的层序BFS和深度优先DFS的栈模拟都在这道题里有一个最朴素的原型。后面你刷到验证二叉搜索树、对称二叉树、二叉树的最小深度、二叉树的直径甚至二叉树的序列化都会反复用到这里练出来的手感。所以别急着“AC完就走”把104题的递归写发、迭代写发、出错原因、变体思路全部过一遍这份功夫后面会成倍回馈给你。2. 第一直觉递归解法深挖2.1 递归代码的每一行是怎么来的递归解法的核心逻辑非常短力扣官方题解给的版本是这样class Solution: def maxDepth(self, root: Optional[TreeNode]) - int: if root is None: return 0 left_depth self.maxDepth(root.left) right_depth self.maxDepth(root.right) return max(left_depth, right_depth) 1这段代码只有五行是真正的逻辑但每一行都必须说得出理由。第一个if判断是递归的出口。树这个结构是递归定义的一个节点左牵右挂分别指向两棵子树子树又各自是树直到指向空节点。递归函数必须处理这个“空”的情况否则会在叶子节点上继续往下访问None.left直接抛AttributeError。left_depth和right_depth这两行本质上是在委托子问题。求整棵树的最大深度等于分别求左右子树各自的最大深度然后取大的那个。这里体现了分治思想最朴素的形式拆成子问题、分别解决、合并答案。注意这里我并没有把结果写回全局变量而是作为返回值逐层向上传递这是纯函数式写法踩坑最少。最后一行max(left_depth, right_depth) 1这1就是当前节点本身。你在子树深度上加上当前这一层才能得到以当前节点为根的整棵子树的高度。忘记加1是新手最常见的错误后果是返回结果永远比正确答案少1。2.2 递归过程可视化一颗最美味的洋葱理解递归最好的方式是随手画一个小的递归过程。假设树长这样3 / \ 9 20 / \ 15 7调用maxDepth(3)时它会先问maxDepth(9)由于9是叶子节点它的左右孩子都是None两个递归调用都返回0于是叶子返回max(0,0)11。再看右侧maxDepth(20)会先算maxDepth(15)得到1再算maxDepth(7)得到1然后返回max(1,1)12。回到根节点3此时left_depth1right_depth2最终max(1,2)13。答案正确。这个过程就像剥洋葱递归调用是一层层往里剥return的时候再从里往外一层层包回去。很多人觉得递归“绕”本质上是没有建立这个“先递后归”的画面。我建议新手拿笔在纸上画一次上述过程代码立刻变透明。2.3 递归解法的复杂度分析与隐患时间复杂度是O(n)因为每个节点都恰好被访问一次空间复杂度是O(height)height是树的高度最坏情况下是一条链递归栈会深度n这也是很多“运行时错误”的源头——当树的节点数达到上万且是一棵退化链状树时递归深度可能触发Python的递归限制默认约1000层或C、Java的栈溢出。这个隐患平时做题不容易暴露因为力扣默认测试数据不会故意给你一条十万层的链。但在真实业务场景比如解析一个极深的JSON结构、遍历一个深层的DOM树这类递归写法就可能直接把调用栈打爆。这也是为什么迭代解法不是一个“能AC就行”的备选而是工程上必须掌握的后手。3. 换一种思路迭代法也能优雅求解3.1 层序遍历BFS把深度数成层数递归解法虽短但面试官问你“能不能不递归写一遍”的时候你至少要有两套预案。最直观的迭代思路是层序遍历。二叉树层序遍历的天然载体是队列。我们逐层把节点放进队列处理完一层深度加1直到队列变空。这里有一个关键细节如何知道“一层”在哪里结束很多人的第一版代码是一股脑把左右孩子入队然后while queue非空就pop一个处理一个结果深度数成了节点总数。正确做法是每次进入循环时先用len(queue)快照当前层的节点数然后只处理这么多节点它们处理过程中新加入的左右孩子属于下一层不在本次循环范围内。from collections import deque class Solution: def maxDepth(self, root: Optional[TreeNode]) - int: if root is None: return 0 q deque([root]) depth 0 while q: depth 1 level_size len(q) for _ in range(level_size): node q.popleft() if node.left: q.append(node.left) if node.right: q.append(node.right) return depth这段代码里depth 1写在每层循环的开头意味着“又处理完了一层”。对一棵每个节点只有左孩子的退化链队列每次循环里只有一个节点循环次数等于节点数返回的depth就等于链长正确。对一棵每个节点都有左右孩子的满二叉树循环次数等于高度也正确。3.2 DFS的迭代写法用栈模拟系统调用除了BFS深度优先搜索同样可以用栈写成迭代版。递归的调用栈是系统帮我们维护的现在自己用栈显式维护“当前节点”和“当前深度”这对信息。class Solution: def maxDepth(self, root: Optional[TreeNode]) - int: if root is None: return 0 stack [(root, 1)] max_depth 0 while stack: node, depth stack.pop() max_depth max(max_depth, depth) if node.left: stack.append((node.left, depth 1)) if node.right: stack.append((node.right, depth 1)) return max_depth栈的LIFO特性决定了遍历顺序是先处理后压入的节点但因为我们求的是深度遍历顺序不影响最终答案所以不需要额外保证“先左后右”。每次压栈时带上depth1弹出时用max_depth记录见过的最深深度。空间复杂度在最坏情况下是O(n)但堆上分配的空间比递归栈好控制得多。3.3 两种迭代方式的选型建议层序BFS的优点是直观、好记忆而且如果后续要处理“每一层的平均值”“每一层最右侧节点”这类题目这套框架可以直接复用。DFS栈写法的优点是空间效率稳定适合处理特别深的树。我的建议是如果只想记一种迭代写法优先记BFS。因为“按层处理”这个语义覆盖了太多二叉树变体题值得形成肌肉记忆。如果想把空间复杂度优化到极致再额外掌握DFS栈版。二者并不冲突都是从“递归太深会爆栈”这个痛点出发的自然产物。4. 常见报错与排查实录4.1 运行时错误到底谁在报错写二叉树程序时新手遇到最多的不是答案错误而是运行时错误——代码一提交屏幕上冒出一串红色提示。按我的经验运行时错误里占比最高的三类是空指针访问、递归栈溢出、死循环。空指针访问的典型场景是判断root非空之后在递归或循环里对一个null节点取左孩子或右孩子。比如有的同学会把递归出口写成if root.left is None and root.right is None: return 1然后递归调用放在这个判断之后结果遇到空节点直接扑街。解决这类问题没有捷径只能养成习惯任何对root.xxx的访问之前先问自己“root可能是None吗”。栈溢出在力扣上往往表现为“RecursionError”或“std::bad_alloc”之类的崩溃。这类问题通常不是题目数据故意使坏而是你的递归出口写错了导致某些分支永远递归下去。排查方法是print大法在函数入口打印当前节点的val立刻看出来它到底朝哪个方向无限深入。4.2 排查技功速查表症状常见原因快速定位手段返回结果比预期小1递归返回时漏了1检查每一层的返回表达式空树返回值是1空子树返回了1检查递归出口处的return 0一直报RecursionError递归出口缺失或条件错误打印节点值观察递归路径答案等于节点总数BFS层与层未区分检查是否用了level_size快照明明逻辑对但超时每次递归重复建树/重复扫描确认每个节点是否只访问一次4.3 一个容易忽略的Python细节在Python里写递归二叉树解法时有一个细节极其容易踩坑默认递归深度限制。Python的sys.setrecursionlimit默认只有约1000层即使你的算法逻辑天衣无缝遇到一棵1000层的链状树也会直接RecursionError。力扣的题目很少让二叉树退化到这种程度但在你自己构造测试用例或者处理某些特殊输入时这个问题就会冒出来。有人会当场sys.setrecursionlimit(1000000)来应付这对做题是可行的但你要明白这只是一种“绕过”不是“解决”。真正的解决是切换到迭代写法。我在实际写业务代码时从来不主动调高这个限制因为递归栈爆掉的风险并不会因为限制调大而消失。5. 从二叉树最大深度延展出去5.1 判断平衡二叉树深度思想的直接应用力扣110题——平衡二叉树就是104题最典型的变体。它的定义是一棵二叉树中每个节点的左右子树高度差的绝对值不超过1。如果你已经能熟练写出求最大深度的递归那么平衡二叉树判断的第一版思路很自然每个节点都算一下左右子树深度然后检查差值。class Solution: def isBalanced(self, root: Optional[TreeNode]) - bool: if root is None: return True def depth(node): if node is None: return 0 return max(depth(node.left), depth(node.right)) 1 left_depth depth(root.left) right_depth depth(root.right) if abs(left_depth - right_depth) 1: return False return self.isBalanced(root.left) and self.isBalanced(root.right)但这段代码存在重复计算问题每个节点都会被上层调用计算深度同时又被递归调用检查平衡时间复杂度退化为O(nlogn)甚至O(n^2)退化树。更优的写法是从底向上边算深度边判断一旦发现不平衡就提前返回-1。这个“后剪枝”的思路核心仍然是对深度计算的深刻理解。5.2 二叉树深度在真实场景中的投影学过数据结构的人可能会问二叉树最大深度在真实业务里到底有什么用答案是凡是需要“判断一个层级结构有多深”的地方都用得上。比如电商的类目树一个大型商超的货架分类体系本质上是一棵多叉树多叉树可以通过左孩子右兄弟表示法转成二叉树求它的最大深度能帮你评估站点层级是否过深、用户需要点击几次才能找到目标商品。再比如程序里的函数调用链、XML/HTML的DOM嵌套层级、文件系统目录深度全都可以抽象成树形结构的深度问题。还有一个在编译原理和结构化存储里更硬核的应用二叉树的序列化与反序列化。当我们把一棵二叉树保存到磁盘或传给别人时需要一种方式把它变成字符串通常的做法是记录前序遍历和空节点标记。反序列化时要正确重建这棵树本质上也依赖对深度和位置关系的数学理解。超市货架的场景里“遍历二叉树”就是从上到下、从左到右盘点库存这个朴素需求的抽象化表达——每层货架对应树的每一层每件商品对应一个节点。5.3 线索二叉树把遍历成本降下来的野心既然提到了遍历二叉树就绕不开“线索二叉树”这个概念。普通二叉树的节点只有左右孩子指针遍历时要么递归、要么手动用栈或队列每次都要临时维护额外信息。线索二叉树的思路是把叶子节点上空闲的左右指针利用起来左指针指向前驱节点右指针指向后继节点。这样做的收益是中序遍历或前序遍历可以不用栈也不用递归顺着线索一路走完。代价是每个节点需要两个额外的标志位来区分“指针指向的是孩子还是线索”。在求最大深度这道题上线索二叉树不适用但它提醒我们一件重要的事树的深度信息本身也可以在构造时维护进节点里比如每个节点额外存一个height字段这样求最大深度就变成O(1)的字段读取——以空间换时间是工程上常见的权衡。6. 刷题攻略从104题开始把二叉树一网打尽6.1 刷题顺序比刷题数量更重要很多人的刷题路径是从编号最小的题开始刷起这是低效的。力扣的二叉树系列有清晰的依赖关系按这个顺序走每一步都在给下一步打地基二叉树的最大深度打底平衡二叉树深度判断的延伸二叉树的最小深度注意与最大深度的边界差异路径总和深度搜索的经典应用翻转二叉树递归框架的镜像操作对称二叉树两棵树同步递归二叉树的层序遍历BFS框架成型把这一组刷完你对二叉树递归的肌肉记忆就建立起来了之后去看其他中难题才会有“原来不过是这个套路换个皮”的感觉。6.2 三道最容易踩坑的相似题对比很多读者刷完104题紧接着去做111题最小深度发现答案不对。这里有个经典陷阱最小深度是指从根节点到最近叶子节点的最短路径节点数如果一个节点只有左子树没有右子树那么右子树的深度不能简单当作0去比较因为空子树不是叶子节点。同样绕人的还有“最大深度”与“节点个数”之间的区别最大深度看层数节点个数看数量。满二叉树第k层的节点数是2^(k-1)整棵树总节点数最多是2^k-1。群里经常有人把深度和节点数搞混问“最大深度为什么不能直接等于节点数”答案就在定义里深度和个数是不同的度量维度。6.3 写二叉树程序时的高频建议根据我这几年在编译原理和数据结构相关项目里的实操经验总结几条写二叉树程序时的高频建议写任何树的递归函数前先想清楚空节点应该返回什么把它当作函数的第一行代码写下来。在代码里特别区分“当前层”和“子树层”的职责比如求深度时1这个动作放在当前层的返回表达式中而不是放在递归调用里。遇到“运行时错误”优先怀疑空指针和递归出口而不是怀疑题目数据。如果递归代码对外层变量有依赖比如把答案存在self.max_depth里注意递归分支之间的变量污染。调试递归时在每个递归函数入口打一行def debug(node, depth): print( * depth, node.val)能极大改善你的调试体感。7. 最后分享一点我的体会104题是我在力扣上反复重刷了很多遍的题。起初我以为自己已经完全掌握但时隔半年再回来我发现可以用更简洁的框架去重新组织它递归版本展示分治的精髓BFS版本展示按层统计的技巧栈版本展示显式管理空间的方法论。同一道题三种思路其实是三个不同的思维杠杆。我建议你刷完104题之后不要急着标记“已掌握”而是把题解区里不同语言的解法各看一遍尤其用Python、Java、C各写一次。语言差异会迫使用不同的思路理解同一个问题比如Python递归方便但要注意限制C迭代栈则自然高效。这种跨语言的对照比刷十道同样难度的题带来的能力提升更大。二叉树的题目是做不完的但核心套路极其有限。把104题真正吃透让它成为你根系的一部分后面的路会走得顺畅很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进