拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

LeetCode 104二叉树最大深度:递归BFS迭代DFS三种解法与避坑指南

LeetCode 104二叉树最大深度:递归BFS迭代DFS三种解法与避坑指南

104题大概是LeetCode hot100里最“容易”又最“坑”的一道题了。说它容易,是因为题目一句话就能看懂:给定一棵二叉树的根节点,返回这棵树的最大深度。说它坑,是因为很多人在本地IDE里跑得好好的,一粘贴到LeetCode上就报运行时错误;还有的人代码逻辑看起来完全没问题,但提交后就是空指针异常。这个现象在hot100题解区特别常见,尤其集中在二叉树的遍历与深度计算这类入门题上。

所以这篇文章不止是给一个标准答案,我想把“二叉树的最大深度”这题从根上拆透。我们会用递归DFS、迭代BFS、迭代DFS三种思路来实现,把每步的返回值、递归终止条件、空间消耗全部讲明白;再专门用一章来回应一个高频痛点——“写二叉树程序时为什么总是报运行时错误”,把空指针、栈溢出、被测环境差异这些坑挨个排掉。适合刚开始刷hot100的读者、准备面试前想系统地过一遍树形递归的人,以及卡在代码运行报错上迟迟想不通的朋友。

1. 题目背后在考什么:最大深度的本质

1.1 先搞清楚二叉树的深度到底怎么定义

LeetCode 104的题目原文很简单:给你一棵二叉树的根节点 root,返回它的最大深度。但“深度”这个词在不同资料里其实是有点小区别的。有的书把根节点的深度定义为0,有的定义为1,LeetCode这道题采用的是后者:从根节点到最远叶子节点的最长路径上的节点数。也就是说,空树深度为0,只有一个根节点的树深度为1,根节点加一个左孩子的树深度为2。

这个定义直接影响了递归终止条件的写法。如果根节点深度定义为0,那递归返回时处理逻辑会略有不同,但核心思想是一致的:逐层往下走,每走一层深度加1,直到遇到空节点。很多人写递归时搞混了“节点数”和“边数”,把最大深度写成节点之间的边条数,导致结果差1。LeetCode的示例里通常会用 [3,9,20,null,null,15,7] 这样的层序序列来表示树,最大深度是3,你数节点数就是3,数边数则是2。动手写之前先把这个基准对齐,后面所有解法才不会跑偏。

另一个容易混淆的概念是“深度”与“高度”。在不少中文教材里,节点的深度是指从根节点到该节点的边数/层数,而节点的高度是指从该节点到最远叶子节点的边数/层数,两者方向不同。但LeetCode 104要的是整棵树的最大深度,从实现角度看,它等价于根节点的高度。所以在讨论这道题时,我一般直接说“最大深度就是整棵树的层数,也就是根节点到最远叶子节点经过的节点总数”,这样最不容易产生歧义。

1.2 为什么这题是hot100的“试金石”

在hot100题库里,二叉树相关题目占了相当大的比例,而104基本是很多人刷树的第一站。它看起来简单,但背后考查的东西一点不少:递归思想是否牢固、对树的遍历是否熟悉、对各种树形态(空树、单节点、斜树、满二叉树)的边界处理是否谨慎。更重要的是,它能测验你对递归调用栈的理解程度。因为最大深度这道题用递归写只要三五行,可是这三五行里一旦少写了一个判空分支,运行时错误就会立刻蹦出来。

面试场景里这题也经常被拿来当“热身题”。面试官会让你手写解法,然后追问“如果树特别深,递归会不会崩?迭代怎么写?”这就从一道简单题直接上升到对复杂度和工程思维的考察。我见过不少候选人递归秒过,但一问到递归栈的深度最坏是多少就卡壳了。所以这篇博文不只是为了通过104,更是为后面刷平衡二叉树、二叉树的直径、路径总和这些hot100题打底。树形递归的“模板感”一旦建立起来,后面很多题目都可以套用。

1.3 换个角度看:最大深度就是层序遍历的层数

除了递归,我们还可以用另一种直觉来理解最大深度:把二叉树想象成一颗洋葱,从根节点开始,每次剥掉当前最外层的一层节点,剥了多少次,深度就是多少。这正是层序遍历的思想。用队列做层次遍历时,每处理完一层,计数器加1,等队列全部清空,计数器的值就是最大深度。

这个视角很有用,因为很多新手被递归绕晕后,可以用层序遍历来“保底验证”。比如你递归写出了答案但不确定对不对,可以再写一个BFS版本跑同一组测试数据,两个结果一致,基本就稳了。而且BFS版本的额外优势是不会占用系统递归调用栈,在树深度很大的情况下不容易栈溢出,实际工程里也更适合处理那种极端的“斜树”场景。后面我会详细展开BFS的实现,这里先记住一个结论:深度既可以用“根到叶的路径节点数”来理解,也可以看成“树的层数”,两种理解对应两类解法。

2. 三种主流解法的选择与原理

2.1 递归DFS:最短代码背后的信任问题

递归是求解最大深度最自然的思路,因为问题的结构本身就是递归的:一棵二叉树的最大深度,等于左子树的最大深度和右子树的最大深度中较大的那个,再加1(加的是根节点自己)。如果用伪代码写,就是:

function maxDepth(node): if node is null: return 0 leftDepth = maxDepth(node.left) rightDepth = maxDepth(node.right) return max(leftDepth, rightDepth) + 1

在Java里的实现也很直接:

public int maxDepth(TreeNode root) { if (root == null) { return 0; } return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; }

很多第一次接触的人会问:为什么空节点返回0而不是1?因为空节点不包含任何节点,不该计入深度。而叶子节点的左右孩子都是空,叶子节点自身返回的深度是 max(0, 0) + 1 = 1,这正好符合“单节点树深度为1”的定义。这个终止条件是整段代码的灵魂,漏掉它或写错它,程序就会无限递归或者返回错误的深度值。

递归DFS之所以让人既爱又恨,是因为它把复杂的遍历过程交给了函数调用栈。你在代码里看不到显式的“遍历”动作,但每一次递归调用都在隐式地向下探索。这种抽象能力是好事,可也意味着你必须充分信任递归的“契约”:函数会返回以当前节点为根的子树最大深度。一旦你在写的时候没有想清楚这个契约,就很容易在多层的递归里绕晕。我的建议是:写这种递归函数前,先把注释写上“返回以node为根的子树的最大深度”,然后用这个定义去推导代码,错误率会低很多。

2.2 迭代BFS:用队列数清楚每一层

如果不想依赖递归,BFS是最符合直觉的替代方案。我们用队列把每一层的节点装进去,然后一层一层地往外扩。处理完一层,深度计数器就加1。整个过程和“剥洋葱”一模一样。

from collections import deque def maxDepth(root): if not root: return 0 queue = deque([root]) depth = 0 while queue: depth += 1 level_size = len(queue) for _ in range(level_size): node = queue.popleft() if node.left: queue.append(node.left) if node.right: queue.append(node.right) return depth

这里有一个非常关键的细节:必须在进入每一层时先记录level_size = len(queue),然后循环处理level_size次。因为队列在循环过程中会不断加入新的节点,如果不提前锁定当前层的节点数量,就会把下一层的节点也在当前轮次里处理掉,导致深度计数错乱。这是层序遍历最常见的一个bug,尤其是在处理完左孩子后又把右孩子加进来时,很容易头脑发热把for _ in range(len(queue))直接写进循环条件里。

BFS的空间复杂度在最坏情况下是O(n),因为队列中最多会同时存在一整层的节点。对于完全二叉树来说,最后一层可能有 n/2 个节点,所以空间占据上是线性的。优点是它不会因为树的深度过大而栈溢出,因为用的是堆内存里的队列,而不是操作系统线程栈。在普通实现里,BFS代码比递归稍长,但胜在逻辑直观,而且这个“按层处理”的框架后面还能直接套用到“二叉树的最大宽度”等题目里。

2.3 迭代DFS:用栈手动维护“当前深度”

BFS用队列天然匹配“层”的概念,DFS则可以用栈来模拟递归过程。既然递归本身就是在系统栈上压栈弹栈,那我们完全可以在堆上自己创建一个栈,把“当前节点”和“走到当前节点时的深度”一起压进去。每次从栈里弹出一个元素时,就用它的深度更新最大深度,然后把它不为空的左右孩子压进栈,孩子的深度是当前深度加1。

def maxDepth(root): if not root: 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

这种写法其实是在模仿递归中的“先访问根,再访问孩子”的前序遍历框架。因为栈是先进后出,所以压栈顺序不影响最终最大深度的正确性,左右孩子谁先谁后都行,反正所有节点都会被访问到,每个节点记录的是“从根到它自己的深度”。栈中需要同时保存节点和深度信息,所以空间复杂度同样是O(n)。相比递归,它的优势是不受系统递归调用栈的限制,在极端深度的斜树上也不会抛StackOverflowError。

还有一些人会写“后序迭代法”,用一个栈模拟递归的完整调用流程,最后弹栈时再更新深度。那样做更贴近编译器递归执行的内部原理,但代码会复杂很多。对于这道题,直接存二元组的方案最简单可靠。我们学习迭代DFS,重点不是背代码,而是理解“栈加状态”这个通用技巧,很多树相关的非递归遍历题都要靠它。

2.4 三种方法复杂度和适用场景对比

做个直观的对比表格,方便你面试时快速回答:

解法时间复杂度空间复杂度核心机制适用场景
递归DFSO(n)最坏O(n),平均O(log n)系统调用栈代码最简洁,适合理解递归思想,面试首选
迭代BFSO(n)最坏O(n)队列按层处理逻辑直观,适合需要知道“每一层信息”的变体题
迭代DFSO(n)最坏O(n)栈保存节点和深度避免系统栈溢出的场景,适合深度很大的树

时间复杂度都是O(n),因为每个节点都需要被访问一次。空间复杂度的差别主要在“最坏情况”。递归解法在树退化成链表时,递归深度等于节点数量,系统栈会消耗O(n)的内存;如果n达到十万级,可能直接栈溢出。BFS和迭代DFS用的是堆内存中的队列/栈,同样最坏需要O(n)空间,但一般不容易触发“调用栈内存不足”这种运行时错误。

这里还要强调一下:很多人说平衡二叉树的空间复杂度是O(log n),这其实是把“树高与节点数的关系”带进来了。对于平衡树,高度是log n量级,递归栈深度也就是O(log n)。但这不是算法本身的固定复杂度,而是取决于输入树的形状。所以在回答面试题时,最好先给最坏情况O(n),再补充说“如果输入是平衡二叉树,递归栈深度会小很多”。这样既严谨,又能展示你的分析能力。

3. 实操过程:从零写出不报错的题解

3.1 先想清楚递归函数的入参和返回值

我刷题的习惯是,拿到题先不急着写代码,先把“这个递归函数到底要做什么”写在注释里。对104题,我会写:

/** * 计算以 node 为根节点的子树的最大深度。 * 如果 node 为空,返回 0。 * 否则返回 max(左子树最大深度, 右子树最大深度) + 1。 */ private int dfs(TreeNode node) { // ... }

明确了入参和返回值之后,代码几乎是被“逼”出来的:先写终止条件if (node == null) return 0;,再写递归调用和聚合逻辑。整个过程不超过两分钟。很多人在LeetCode上敲代码时喜欢直接开始写if (root.left != null)这种显式判空,反而把简单问题复杂化了。使用递归的优雅之处就在于,每次都只关心当前节点和它的左右孩子,不需要在当前这一层去判断孙子节点是否存在——那是下一层递归要做的事情。

但有一类错误就是从这里来的:如果在递归函数内部,你总是先判断node.left != null再递归,那你必须同时处理“node本身为空”的情况。最常见的“运行时错误”是忘了最外层的空树判断,或者在某一层递归中访问了null.left。记住:递归的首要任务是把空节点的返回条件写好,而不是在每个地方都加判空。每个递归调用进来,第一行永远是检查当前节点是否为空。

3.2 本地调试代码准备:别在LeetCode里裸奔

初学者很喜欢直接在LeetCode网页上的代码编辑器里写代码,写完立刻点提交。这种效率当然高,但也很容易因为一个低级错误反复试错。我建议本地IDE里准备一套能直接跑起来的Java或Python模板,先在本地调试,确认无误后再搬到LeetCode。Java版本需要一个简单的TreeNode类和main方法做测试:

public class Solution { public int maxDepth(TreeNode root) { if (root == null) { return 0; } return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; } public static class TreeNode { int val; TreeNode left; TreeNode right; TreeNode() {} TreeNode(int val) { this.val = val; } TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; } } public static void main(String[] args) { Solution solution = new Solution(); // 构造一棵树: [3,9,20,null,null,15,7] TreeNode root = new TreeNode(3); root.left = new TreeNode(9); root.right = new TreeNode(20); root.right.left = new TreeNode(15); root.right.right = new TreeNode(7); System.out.println(solution.maxDepth(root)); // 期望输出 3 System.out.println(solution.maxDepth(null)); // 期望输出 0 } }

LeetCode环境本身就内置了TreeNode类,所以提交时你只需要粘贴Solution类里的maxDepth方法,不用贴TreeNode定义。这是很多人第一次提交报错的原因之一:把本地用的TreeNode定义也粘贴上去了,导致类重复定义或者编译失败。建议在本地调试时把方法单独放在一个类里,提交时只提交那个方法。

3.3 测试用例与预期结果:把边界情况喂饱

这一道题测试用例不多,但边界情况必须齐全。我每次写二叉树题目都会建立一套固定的用例集合:

用例描述层序表示预期最大深度
空树null0
单节点[1]1
左斜树[1,2,null]2
右斜树[1,null,2]2
完全二叉树[3,9,20,null,null,15,7]3
更深的不平衡树[1,2,3,4,null,null,5]3

为什么一定要测空树和斜树?空树测的是终止条件是否写对了;斜树测的是递归深度是否能正确累积,同时也能提醒你,如果树节点数很多,递归是否可能栈溢出。我用递归版答案跑这些用例,前几个都很顺利,但当我用本地循环生成一个10000层斜树去测的时候,Java直接抛出了StackOverflowError。这正是面试官爱追问的点:递归不是银弹,极端数据下会崩。

BFS和迭代DFS版本在处理10000层斜树时则没有任何问题。这不是说递归写法有问题,只是我们需要知道每个答案的边界在哪。如果你在LeetCode上只跑官方给的测试用例,可能永远不会触发栈溢出,因为官方用例不会刻意构造超深树;但如果你额外去力扣的测试集边缘试探,或者自己拿大型数据测,就能暴露问题。刷题不能只求“通过了”,要有意识地验证自己的算法在极端输入的鲁棒性。

3.4 复杂度计算的完整推导

访问每个节点恰好一次,所以时间复杂度是O(n),其中n是二叉树节点数。递归的空间复杂度计算要分两步看:每一帧调用需要常数级内存,递归的最大深度等于树的高度h,所以空间复杂度是O(h)。在最坏情况下,树退化成一个链表,h等于n,也就是O(n);在最好/平均情况下,如果是平衡二叉树,h约等于log n,也就是O(log n)。

BFS的空间复杂度是队列中最多同时存储的节点数,也就是树的最大宽度。完全二叉树最后一层大约有n/2个节点,所以最坏也是O(n)。迭代DFS的栈中最多存储的节点数量同样和树的形态有关,最坏情况下斜树会一直把右孩子压栈,栈中保存的节点数量也是O(n)。这三个方案的时间复杂度一模一样,空间复杂度的最坏情况也都是O(n),所以在LeetCode判题结果上,三者通常都会通过,速度差异很小。此时选择哪种写法,主要看你更想展示递归思想,还是更想证明自己掌握了迭代写法。

我还经常被问到“能不能做到更快”。答案是,找最大深度必须看完整棵树,至少访问一次所有节点,所以O(n)已经是最优时间复杂度。如果有人非要说“剪枝优化”,那是针对特定问题形态的,比如找“最浅深度”时可以在遇到叶子节点后提前终止;找“最大深度”的时候,任何节点都可能通向更深的路径,剪不掉。

4. 常见问题:写二叉树程序为什么总是报运行时错误

4.1 运行时错误第一号:空指针访问

在LeetCode上,104题最常见的运行时错误就是java.lang.NullPointerException,Python 则是AttributeError: 'NoneType' object has no attribute 'left'。原因基本一致:递归到空节点时,代码仍然尝试访问它的左右孩子。我见过一个典型的错误写法:

public int maxDepth(TreeNode root) { if (root.left == null && root.right == null) { return 1; } return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; }

这段代码在root为叶子节点时没问题,但如果root本身就是null,第一行root.left就直接空指针了。就算root不为空,只要某一层的某个孩子为null,递归调用时也会在root.left那里崩溃。这类问题的根源是“先访问再判空”和“递归终止条件只覆盖叶子节点,没覆盖空节点”。

解决思路非常简单:把终止条件统一成if (root == null) return 0;。这样一来,叶子节点的左右孩子会被递归调用,但进入空节点后直接返回0,就不会再有任何空指针访问。这个调法能让所有后续对root.left或root.right的访问都发生在“当前节点非空”的上下文里,从根上杜绝了空指针。

4.2 运行时错误第二号:递归没有出口导致栈溢出

如果你遇到的是StackOverflowError,说明递归一直在无节制地压栈。常见的原因有两个。第一是没有写终止条件,或者终止条件永远不成立,比如if (root == null) return 1;这种写错返回值的也会导致逻辑异常,但栈溢出主要来自缺失有效的递归出口。第二是输入树本身深度过大,比如节点数达到几万甚至几十万,而递归深度就是节点数,Java线程栈默认大小通常只有512KB到1MB,每帧至少占用几十字节,递归个几万层就撑爆了。

这种问题在LeetCode上不一定常见,因为官方测试用例的树深度通常控制在一个合理范围内。但你在本地自测时很容易自己构造一个大斜树,然后疯狂报错。要区分是代码问题还是输入问题,可以先把递归版在斜树上测一测,如果100层能跑,1万层就崩,那么代码逻辑多半没问题,是系统栈的限制。如果真的需要处理超深树,就改用BFS或迭代DFS。我还遇到过一些人把递归函数写在main函数里,用局部类超多导致每次递归都会创建新对象,也加剧了内存开销,但这种属于写法太绕,不推荐。

4.3 运行时错误第三号:本地能跑,LeetCode却报错

这一条是最让人摸不着头脑的。本地IDE里明明能运行,粘贴到LeetCode后却编译失败或执行出错。通常逃不开几种原因:

  • 本地类名是Main或者任意类名,但LeetCode要求提交的类必须是Solution,方法签名必须和题目一致。
  • 本地把TreeNode类又定义了一遍,LeetCode已经内置了TreeNode,重复定义直接编译失败。
  • 本地用了package包声明,提交时没有去掉,导致编译错误。
  • 本地代码里带了public static void main方法,虽然LeetCode允许,但有时候多余代码会干扰阅读,提交前最好删掉只留核心方法。

此外,注意主方法签名里的参数类型是TreeNode,不是Node,也不是自定义的内部类。力扣做题时,顶部通常已经有Definition for a binary tree node.注释块,里面定义了TreeNode。你只需要实现Solution类中的方法,不要修改题目给的TreeNode定义,更不要写一个同名的TreeNode类。

4.4 排查技巧速查表

报错关键信息大概率原因解决方案
NullPointerException/AttributeError对空节点访问属性递归第一行加上空节点返回0
StackOverflowError递归无出口或树深过大检查终止条件;改用迭代BFS/DFS
Compile Error类名/方法签名不对,或重复定义TreeNode确保类名为Solution,删掉自定义TreeNode
输出结果为1或总是少1深度定义理解错误用“节点数”而非“边数”计算
BFS结果不对每层循环用了变化的len(queue)进入循环前先n = len(queue)

4.5 一个高级坑:全局变量在多测试用例之间污染

有些读者不喜欢写递归返回值,而是用一个全局变量记录最大深度。比如在maxDepth方法里先定义一个int maxDepth = 0;但这是局部变量,没问题。问题出在把maxDepth定义成Solution类的成员变量:

public class Solution { private int max = 0; public int maxDepth(TreeNode root) { traverse(root, 1); return max; } private void traverse(TreeNode node, int depth) { if (node == null) return; max = Math.max(max, depth); traverse(node.left, depth + 1); traverse(node.right, depth + 1); } }

这个代码在单个测试用例里是对的,但LeetCode执行测试时不会为每个例子重新创建Solution对象,有时候会复用同一个实例跑多个用例。如果max没有在方法开头重置,第二个用例的结果就可能残留第一个用例的值,导致答案偏大。正确做法是在maxDepth方法内部先用局部变量初始化,或者传入一个“当前记录最大值”的引用,或者干脆像最简递归那样直接用返回值累加。我个人的习惯是:树的递归题优先用“返回值”传递状态,而不是用成员变量,这样更不容易踩到多用例污染。

5. 从最大深度延伸出去的二叉树体系

5.1 最大深度与各种遍历方式的关系

很多人在刷hot100时会看到“二叉树的遍历”这类热词。坦白说,最大深度这道题并不要求你会写中序遍历,但如果你想彻底掌握树形题目,必须理解深度计算和各种遍历的关系。前序遍历非常适合递归计算深度:访问当前节点时,深度就已经到了某个值,然后往下传。中序遍历也能算出深度,但中序的“访问顺序”不是按层来推进的,计算深度时需要额外记录当前层数,反而别扭。后序遍历则是最自然的递归方案:先算左子树深度,再算右子树深度,最后综合出当前节点深度——104题解法本质就是后序思想的体现。

层序遍历和BFS正相关,前面已经详细写过。你还会发现,前序、中序、后序、层序都绕不开“每个节点都要访问”的约束,所以在复杂度上,所有解法都是O(n)。真正不同只是“访问顺序”和“状态传递方式”。理解了这个,你在面对更多二叉树题目时就不会再纠结“用哪种遍历”,而是会想“这道题需要什么顺序的信息”,比如判断对称二叉树需要同时比较左右子树对应位置,而计算直径需要后序遍历先拿到左右子树的高度。

5.2 相关hot100题目与变体清单

最大深度的代码模板稍微改一改,就能解不少hot100题。最典型的是“平衡二叉树”题,思路是把104的递归结果应用到每个节点上,判断左右子树深度差是否超过1;“二叉树直径”题则是在后序遍历时同时维护一个全局最大值,记录的其实是左子树深度加右子树深度;“路径总和”题是判断是否存在从根到叶子路径的和等于目标值,它的递归终止条件会用到“叶子节点”的判断,比“空节点”判断更复杂一档。

我把这些变体列出来,不是让大家现在就去刷,而是想说:104是树形递归的最小可用模型。你把这个模型的“递归返回值”和“全局更新”两条线索理清了,后面遇到任何需要“自底向上收集子树信息”的题目,都能迅速找到思路。比如“打家劫舍 III”这类树形DP,本质上也是递归返回两个状态,再合并计算,套路和求最大深度非常接近。

5.3 动态规划视角:二叉树上的“递推思想”

hot100热搜词里有“hot100动态规划”,很多人会觉得二叉树和动态规划是两回事,其实它们是相通的。最大深度的递归公式可以写成:f(node) = max(f(node.left), f(node.right)) + 1,这本身就是一种状态转移方程,只不过是在树形结构上做自底向上的递推。动态规划里的“自顶向下带备忘录”对应递归加缓存,“自底向上填表”对应后序遍历把子结果返回给父节点。

当然,求最大深度用不上缓存,因为每个节点只被访问一次,没有重叠子问题。但真正的树形DP,比如“二叉树中的最大路径和”“监控二叉树”这类hot100延伸题,就是在这个递归框架上增加更多状态变量。所以我推荐刷题时把这个最简单的递推想清楚:问题能不能分解成规模更小的子问题?子问题的解如何合并?边界是什么?这三个问题想明白,树形动态规划的大门就打开了。

5.4 搜索二叉树、线索二叉树中深度概念的分量

热搜词里还有“搜索二叉树”和“线索二叉树”。二叉搜索树(BST)的操作复杂度与树的高度直接挂钩:一棵平衡BST的查找、插入、删除都是O(log n),一旦退化成斜树,就变成O(n)。理解104这道题,会帮助你意识到为什么平衡树那么重要——高度就是生命线。AVL树和红黑树的核心工作,就是通过旋转把树的高度控制在O(log n)以内,从而保证效率。

线索二叉树则是把空闲的左右孩子指针利用起来,指向遍历序列的前驱和后继,这样遍历就不需要递归或栈了。但这个设计并没有改变树的深度结构问题,树依然可能很斜,线索化只是让“寻找下一个遍历节点”变快了,并没有让树变矮。从这个延伸来看,深度的概念贯穿了几乎所有二叉树体系:无论是优化查找性能,还是简化遍历流程,最终都要回到“这棵树有多高”这个根本问题上来。所以,把104题弄扎实,等于给整棵“二叉树知识树”打了地基。

写到这里,我不禁想起自己第一次刷104时的状态:三行代码写完,提交,报错,再看一眼,原来是忘了空树返回0。后来陆陆续续把BFS、迭代DFS都写完,再把直径题、平衡树题刷透,才发现这道“简单题”里的门道其实足够消化一整周。我现在写任何二叉树递归题,第一行永远是判空,永远先想清楚“这个函数返回什么”,这两个习惯就是从104题的坑里养出来的。希望这篇文章能让你少走一点弯路,也希望大家在面对“二叉树的最大深度”时,不只是记住答案,而是真正理解它背后的递归、遍历与边界。

返回列表