bigywater.tech
算法与数据结构 · LeetCode 104

LeetCode 104. 二叉树的最大深度

给定一棵二叉树,求从根结点到最远叶子结点的最长路径上的结点数(也就是树的高度)。标准递归写法很简单:1 + max(左子树深度, 右子树深度)。但如果改用栈做迭代 DFS,一个常见陷阱是把"访问过的结点总数"当成"树的高度"——每弹出一个结点就把计数加一。对链状的树这恰好蒙对,但对结点多、树矮的"宽"树会差出好几倍。本文用一组不断增大的完全二叉树验证这个规律,给出正确写法、5 种语言实现,以及递归展开与回溯的分步演示。

127 vs 7
127 个结点的完全二叉树,陷阱写法输出 127,正确高度其实只有 7
4 步
分步演示:先递归展开左右子树,再取较大值加一回溯,另附 5 种语言实现

题意与示例

本文只用自己的话概括题意,完整约束以官方页面为准。给定一棵二叉树,返回它的最大深度:从根结点到最远叶子结点的最长路径上的结点数。一共 5 组用例,覆盖:官方示例、空树、单结点、只有右孩子的链、完全二叉树:

真实实测 · 本机执行输出
[3,9,20,null,null,15,7]  →  3
[]  →  0
[1]  →  1
[1,null,2]  →  2
[1,2,3,4,5,6,7]  →  3

陷阱:把"访问过的结点数"当成"树的高度"

标准递归写法是 1 + max(左子树深度, 右子树深度),非常直接。但如果改用迭代、拿一个栈做 DFS,一个容易踩的坑是:把"深度"和"访问过的结点数量"搞混——每从栈里弹出一个结点就把 depth 加 1,这样数的其实是总共访问了多少个结点,而不是树到底有多高。在结点很多但树很矮(很"宽")的情况下,这两个数字会差得很远:

真实实测 · 本机执行输出
输入 [1,2,3,4,5,6,7](7 个结点的完全二叉树,真正的高度是 3)\n正确写法(递归求高度)      →  3\n陷阱写法(每弹出一个结点 +1) →  7

用一组不断增大的完全二叉树验证这个规律:

真实实测 · 本机执行输出
完全二叉树规模 n 与两种写法的输出:\nn=  1  正确高度=1  陷阱输出(结点数)=  1\nn=  3  正确高度=2  陷阱输出(结点数)=  3\nn=  7  正确高度=3  陷阱输出(结点数)=  7\nn= 15  正确高度=4  陷阱输出(结点数)= 15\nn= 31  正确高度=5  陷阱输出(结点数)= 31\nn= 63  正确高度=6  陷阱输出(结点数)= 63\nn=127  正确高度=7  陷阱输出(结点数)=127

陷阱写法输出的数字始终等于树的结点总数 n,而正确高度只是 log₂(n+1),两者的差距随着 n 增大而线性拉开。对于链状的树(每个结点只有一个孩子),结点数恰好等于高度,这种情况下两种写法碰巧给出一样的结果,容易让人误以为写法是对的——直到遇到一棵比较"宽"的树才会暴露问题。

自底向上递归:五种语言的实现

maxDepth(root):空树深度是 0;否则是 1 + max(左子树深度, 右子树深度),左右子树的深度各自也用同样的方式递归求出。时间 O(n),空间是递归深度 O(h)。Python、Java、Go、JavaScript、C++ 都是真实编译运行的,用同一组 5 个测试用例。

Python · 解法代码def max_depth(root): if root is None: return 0 return 1 + max(max_depth(root.left), max_depth(root.right))
真实实测 · Python · 前几组用例的输出
$ python3 solution.py
[3,9,20,null,null,15,7]  →  3
[]  →  0
[1]  →  1
[1,null,2]  →  2
[1,2,3,4,5,6,7]  →  3
Java · 解法代码class Solution { public int maxDepth(TreeNode root) { if (root == null) return 0; return 1 + Math.max(maxDepth(root.left), maxDepth(root.right)); } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java && java -cp build Main
[3,9,20,null,null,15,7]  →  3
[]  →  0
[1]  →  1
[1,null,2]  →  2
[1,2,3,4,5,6,7]  →  3
Go · 解法代码func maxDepth(root *TreeNode) int { if root == nil { return 0 } l, r := maxDepth(root.Left), maxDepth(root.Right) if l > r { return l + 1 } return r + 1 }
真实实测 · Go · 前几组用例的输出
$ go run solution.go
[3,9,20,null,null,15,7]  →  3
[]  →  0
[1]  →  1
[1,null,2]  →  2
[1,2,3,4,5,6,7]  →  3
JavaScript · 解法代码function maxDepth(root) { if (root === null) return 0; return 1 + Math.max(maxDepth(root.left), maxDepth(root.right)); }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js
[3,9,20,null,null,15,7]  →  3
[]  →  0
[1]  →  1
[1,null,2]  →  2
[1,2,3,4,5,6,7]  →  3
C++ · 解法代码class Solution { public: int maxDepth(TreeNode* root) { if (!root) return 0; return 1 + max(maxDepth(root->left), maxDepth(root->right)); } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin
[3,9,20,null,null,15,7]  →  3
[]  →  0
[1]  →  1
[1,null,2]  →  2
[1,2,3,4,5,6,7]  →  3
真实实测 · 本机执行输出
共 5 组用例(含边界),以 Python 结果为基准逐组比对:\n  Python      通过 5/5\n  Java        通过 5/5\n  Go          通过 5/5\n  JavaScript  通过 5/5\n  C++         通过 5/5

随机对拍

Python 版本的递归实现另外和一个独立写的 BFS 版本做了随机对拍:BFS 版用队列按层扩展,每完整处理完一层就把深度计数加 1,处理到队列为空为止。

真实实测 · 本机执行输出
随机对拍 30,000 组(结点数 0 到 12 的随机二叉树,值取 0/1),递归版与 BFS 版的结果全部一致,0 处不一致。

另外四种语言只在上面 5 组固定用例上验证过,没有做大规模随机对拍。

分步演示:先展开子树,再取较大值回溯

下面的演示用官方示例 [3,9,20,null,null,15,7]:递归从根结点展开,先算左子树的深度,再算右子树的深度,最后取较大值加一回溯给父结点。蓝色是正在展开、还没得到结果的结点,绿色是已经求出深度、正在往上回溯的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的。

最大深度:先展开子树,再取较大值回溯
正在递归展开的结点已求出深度、正在回溯的结点

    参考与说明

    • 官方题目页:LeetCode 104. 二叉树的最大深度。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:正确的迭代写法(用队列按层计数,或栈里存 (结点, 深度) 二元组)。
    京ICP备2026067502号-1京公网安备11010502063487号