给定一棵二叉树,求从根结点到最近叶子结点的最短路径上的结点数。最大深度的写法是 1+max(左,右),很自然会想照搬成 1+min(左,右) 来求最小深度,但这里有个陷阱:如果一个结点只有一侧孩子,空的那一侧深度是 0,min(0, 另一侧) 永远是 0,会把这个根本不是叶子的结点误判成深度 1。本文用一条只有右孩子的链验证这个错误,并用 30000 组随机树证明超过六成的树会踩到这个坑,再给出正确写法、5 种语言实现,以及分类讨论的分步演示。
本文只用自己的话概括题意,完整约束以官方页面为准。给定一棵二叉树,求它的最小深度:从根结点到最近叶子结点的最短路径上的结点数(叶子结点指没有左右孩子的结点)。一共 5 组用例,覆盖:官方示例、只有右孩子的链、空树、单结点、只有左孩子的两结点树:
[3,9,20,null,null,15,7] → 2 [2,null,3,null,4] → 3 [] → 0 [1] → 1 [1,2] → 2
上一道"最大深度"题的写法是 1 + max(左子树深度, 右子树深度),很自然会想照搬成 1 + min(左子树深度, 右子树深度) 来求最小深度。但这里有个陷阱:如果一个结点只有一侧孩子(比如只有右孩子,左孩子是空),空的那一侧深度按定义会返回 0,min(0, 右子树深度) 永远是 0,会让这个"只有一个孩子、根本不是叶子"的结点被误判成深度为 1 的叶子。用一条只有右孩子的链验证:
输入 [2,null,3,null,4](一条只往右延伸的链,唯一的叶子是最底下的 4)\n正确写法(特判只有一侧孩子的情况) → 3\n陷阱写法(照搬 maxDepth,直接取 min) → 1
陷阱写法在根结点这一步就出错了:根 2 只有右孩子 3,没有左孩子,min(minDepth(左=空)=0, minDepth(右)=2) 算出来是 0,加 1 之后变成 1——意思是"根本身就是最近的叶子",但根其实根本不是叶子(它有一个孩子)。为了确认这不是个例,做了一次随机测试:
30,000 组随机二叉树:\n正确写法与陷阱写法结果不同的有 18,761 组,占 62.5%
超过六成的随机树会踩到这个坑——只要树里存在任何一个"只有单侧孩子"的结点(这在随机生成的树里非常常见),陷阱写法就会把这个结点误判成叶子,给出一个偏小的错误答案。空子树的深度是 0,但"只有一个孩子的结点"不是叶子,不能直接参与 min 比较,必须先特判、强制走向那个唯一存在的孩子。
minDepth(root):空树深度 0;没有左右孩子(真正的叶子)深度 1;只有一侧孩子时,必须强制走向那一侧,不能把空的另一侧算进 min;两侧都有孩子时才是 1 + min(左, 右)。时间 O(n),空间是递归深度 O(h)。Python、Java、Go、JavaScript、C++ 都是真实编译运行的,用同一组 5 个测试用例。
Python · 解法代码def min_depth(root): if root is None: return 0 if root.left is None and root.right is None: return 1 if root.left is None: # 只有右孩子:不能算 min(0, 右),必须走右边 return 1 + min_depth(root.right) if root.right is None: # 只有左孩子:同理必须走左边 return 1 + min_depth(root.left) return 1 + min(min_depth(root.left), min_depth(root.right))
$ python3 solution.py [3,9,20,null,null,15,7] → 2 [2,null,3,null,4] → 3 [] → 0 [1] → 1 [1,2] → 2
Java · 解法代码class Solution { public int minDepth(TreeNode root) { if (root == null) return 0; if (root.left == null && root.right == null) return 1; if (root.left == null) return 1 + minDepth(root.right); // 只有右孩子:必须走右边 if (root.right == null) return 1 + minDepth(root.left); // 只有左孩子:必须走左边 return 1 + Math.min(minDepth(root.left), minDepth(root.right)); } }
$ javac -d build Main.java && java -cp build Main [3,9,20,null,null,15,7] → 2 [2,null,3,null,4] → 3 [] → 0 [1] → 1 [1,2] → 2
Go · 解法代码func minDepth(root *TreeNode) int { if root == nil { return 0 } if root.Left == nil && root.Right == nil { return 1 } if root.Left == nil { return 1 + minDepth(root.Right) // 只有右孩子:必须走右边 } if root.Right == nil { return 1 + minDepth(root.Left) // 只有左孩子:必须走左边 } l, r := minDepth(root.Left), minDepth(root.Right) if l < r { return l + 1 } return r + 1 }
$ go run solution.go [3,9,20,null,null,15,7] → 2 [2,null,3,null,4] → 3 [] → 0 [1] → 1 [1,2] → 2
JavaScript · 解法代码function minDepth(root) { if (root === null) return 0; if (root.left === null && root.right === null) return 1; if (root.left === null) return 1 + minDepth(root.right); // 只有右孩子:必须走右边 if (root.right === null) return 1 + minDepth(root.left); // 只有左孩子:必须走左边 return 1 + Math.min(minDepth(root.left), minDepth(root.right)); }
$ node run.js [3,9,20,null,null,15,7] → 2 [2,null,3,null,4] → 3 [] → 0 [1] → 1 [1,2] → 2
C++ · 解法代码class Solution { public: int minDepth(TreeNode* root) { if (!root) return 0; if (!root->left && !root->right) return 1; if (!root->left) return 1 + minDepth(root->right); // 只有右孩子:必须走右边 if (!root->right) return 1 + minDepth(root->left); // 只有左孩子:必须走左边 return 1 + min(minDepth(root->left), minDepth(root->right)); } };
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin [3,9,20,null,null,15,7] → 2 [2,null,3,null,4] → 3 [] → 0 [1] → 1 [1,2] → 2
共 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 版按层扩展,一旦在某一层遇到第一个真正的叶子结点(没有左右孩子)就立刻返回当前层数,天然规避了"只有单侧孩子"的陷阱,因为它判断的是"两个孩子都不存在"而不是简单取 min。
随机对拍 30,000 组(结点数 0 到 12 的随机二叉树),递归版与 BFS 版的结果全部一致,0 处不一致。
另外四种语言只在上面 5 组固定用例上验证过,没有做大规模随机对拍。
下面的演示用官方示例 [3,9,20,null,null,15,7]:这棵树里每个内部结点都同时有左右孩子,所以不会触发"只有单侧孩子"的特判分支,但代码在每一步都做了检查。蓝色是正在展开的结点,绿色是已经求出深度的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的——建议对照本文"陷阱"一节里 [2,null,3,null,4] 这条链,理解特判分支到底在防什么。