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

LeetCode 111. 二叉树的最小深度

给定一棵二叉树,求从根结点到最近叶子结点的最短路径上的结点数。最大深度的写法是 1+max(左,右),很自然会想照搬成 1+min(左,右) 来求最小深度,但这里有个陷阱:如果一个结点只有一侧孩子,空的那一侧深度是 0,min(0, 另一侧) 永远是 0,会把这个根本不是叶子的结点误判成深度 1。本文用一条只有右孩子的链验证这个错误,并用 30000 组随机树证明超过六成的树会踩到这个坑,再给出正确写法、5 种语言实现,以及分类讨论的分步演示。

62.5%
30,000 组随机二叉树里,有 62.5% 会因为"只有单侧孩子"的陷阱给出错误的最小深度
4 步
分步演示:每个结点先判断两侧孩子是否都存在,只有单侧孩子必须强制走那一侧,另附 5 种语言实现

题意与示例

本文只用自己的话概括题意,完整约束以官方页面为准。给定一棵二叉树,求它的最小深度:从根结点到最近叶子结点的最短路径上的结点数(叶子结点指没有左右孩子的结点)。一共 5 组用例,覆盖:官方示例、只有右孩子的链、空树、单结点、只有左孩子的两结点树:

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

陷阱:照搬 maxDepth 的写法,直接 1+min(左,右),没有特判只有一侧孩子的情况

上一道"最大深度"题的写法是 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))
真实实测 · Python · 前几组用例的输出
$ 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)); } }
真实实测 · Java · 前几组用例的输出
$ 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 · 前几组用例的输出
$ 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)); }
真实实测 · JavaScript · 前几组用例的输出
$ 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)); } };
真实实测 · C++ · 前几组用例的输出
$ 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] 这条链,理解特判分支到底在防什么。

最小深度:两侧都要检查,只有单侧孩子必须强制走那一侧
正在展开的结点已求出深度的结点只有一侧孩子,特判强制走这一侧

    参考与说明

    • 官方题目页:LeetCode 111. 二叉树的最小深度。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:正确的 BFS 写法(找到第一个叶子就提前返回,本文用它做了随机对拍验证)。
    京ICP备2026067502号-1京公网安备11010502063487号