【LeetCode】111. 二叉树的最小深度
文章目录
- 111. 二叉树的最小深度
- 题目描述
- 示例 1:
- 示例 2:
- 提示:
- 解题思路
- 问题深度分析
- 问题本质
- 核心思想
- 关键难点分析
- 典型情况分析
- 算法对比
- 算法流程图
- 主算法流程(递归DFS)
- BFS层序遍历流程
- 复杂度分析
- 时间复杂度详解
- 空间复杂度详解
- 关键优化技巧
- 技巧1:递归DFS(最优解法)
- 技巧2:迭代BFS(找到第一个叶子节点即返回)
- 技巧3:迭代DFS(使用栈)
- 技巧4:递归DFS(简化版)
- 边界条件处理
- 边界情况1:空树
- 边界情况2:单节点树
- 边界情况3:只有一侧子树
- 边界情况4:链状树
- 边界情况5:完全二叉树
- 测试用例设计
- 基础测试用例
- 进阶测试用例
- 常见错误和陷阱
- 错误1:没有处理只有一侧子树的情况
- 错误2:没有检查叶子节点
- 错误3:BFS没有找到第一个叶子节点就返回
- 错误4:深度计算错误
- 实用技巧
- 进阶扩展
- 扩展1:返回最小深度路径
- 扩展2:计算所有叶子节点的深度
- 扩展3:找到最小深度的所有叶子节点
- 扩展4:最小深度与最大深度的关系
- 应用场景
- 总结
- 完整题解代码
111. 二叉树的最小深度
题目描述
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:
输入:root = [2,null,3,null,4,null,5,null,6]
输出:5
提示:
- 树中节点数的范围在 [0, 10^5] 内
- -1000 <= Node.val <= 1000
解题思路
问题深度分析
这是经典的二叉树最小深度问题,核心在于理解最小深度的定义和掌握递归与迭代两种遍历方式。虽然题目看起来简单,但它是理解二叉树、递归思维和树形结构遍历的重要题目。
问题本质
给定一个二叉树,需要计算从根节点到最近叶子节点的最短路径上的节点数。关键问题:
- 深度定义:从根节点到当前节点的路径长度
- 叶子节点:没有子节点的节点
- 最小深度:到最近叶子节点的最短路径
- 递归子结构:树的最小深度需要特殊处理,不能简单取min(左子树深度, 右子树深度)
关键区别:
- 最大深度:max(左深度, 右深度) + 1(即使一侧为空也取另一侧)
- 最小深度:需要特殊处理,如果一侧为空,不能取0,必须取另一侧
核心思想
方法一:递归DFS(最优解法)
- 递归定义:
- 空树:返回0
- 叶子节点:返回1
- 只有一侧子树:返回另一侧深度+1
- 两侧都有:返回min(左深度, 右深度) + 1
- 终止条件:root == nil 返回 0
- 特殊处理:必须处理只有一侧子树的情况
方法二:迭代BFS(层序遍历)
- 使用队列:逐层遍历二叉树
- 找到第一个叶子节点:一旦找到叶子节点,立即返回当前深度
- 队列为空:遍历结束,返回深度
方法三:迭代DFS(使用栈)
- 使用栈:存储节点和对应深度
- 维护最小深度:遍历过程中更新最小值
- 栈为空:遍历结束,返回最小深度
方法四:递归DFS(简化版)
- 简化递归:使用更简洁的递归逻辑
- 处理空子树:使用特殊值标记空子树
关键难点分析
难点1:只有一侧子树的情况
- 如果左子树为空,右子树不为空,最小深度不能是1(根节点)
- 必须继续向下找到叶子节点
- 错误示例:
[1,2]的最小深度不是1,而是2
难点2:叶子节点的判断
- 叶子节点:左右子树都为空
- 最小深度必须到达叶子节点,不能只到中间节点
难点3:与最大深度的区别
- 最大深度:即使一侧为空,也取另一侧
- 最小深度:如果一侧为空,必须取另一侧(不能取0)
典型情况分析
情况1:完全二叉树
3/ \9 20/ \15 7
- 节点9是叶子节点,深度为2
- 节点15和7是叶子节点,深度为3
- 最小深度 = 2(到节点9)
情况2:链状树
2\3\4\5\6
- 最小深度 = 5(到节点6)
- 所有节点都只有右子树,必须到最底层
情况3:只有一侧子树
1/
2
- 最小深度 = 2(到节点2)
- 不能返回1,因为节点1不是叶子节点
情况4:单节点树
1
- 最小深度 = 1(节点1是叶子节点)
情况5:空树
null
- 最小深度 = 0
算法对比
| 算法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 递归DFS | O(n) | O(h) | 最优解法,代码简洁 |
| 迭代BFS | O(n) | O(w) | 找到第一个叶子节点即返回 |
| 迭代DFS(栈) | O(n) | O(h) | 需要遍历所有节点 |
| 递归DFS简化 | O(n) | O(h) | 代码更简洁 |
注:n为节点数,h为树高度,w为树的最大宽度
算法流程图
主算法流程(递归DFS)
graph TDA[minDepth(root)] --> B{root==nil?}B -->|是| C[return 0]B -->|否| D{是叶子节点?}D -->|是| E[return 1]D -->|否| F{只有左子树?}F -->|是| G[return minDepth left + 1]F -->|否| H{只有右子树?}H -->|是| I[return minDepth right + 1]H -->|否| J[return min minDepth left, minDepth right + 1]
BFS层序遍历流程
复杂度分析
时间复杂度详解
递归DFS算法:O(n)
- 需要访问树的所有节点
- 每个节点访问一次
- 每次访问进行常数时间操作
- 总时间:O(n)
迭代BFS算法:O(n)
- 最坏情况需要访问所有节点
- 最好情况找到第一个叶子节点即返回
- 平均情况:O(n)
- 总时间:O(n)
迭代DFS算法:O(n)
- 需要访问树的所有节点
- 每个节点访问一次
- 每次访问进行常数时间操作
- 总时间:O(n)
空间复杂度详解
递归DFS算法:O(h)
- 递归调用栈深度为树高度
- 最坏情况(链状树):O(n)
- 最好情况(平衡树):O(log n)
- 总空间:O(h)
迭代BFS算法:O(w)
- 需要队列存储节点
- 最坏情况(完全二叉树):队列大小为叶子节点数,约为n/2
- 最好情况(链状树):O(1)
- 总空间:O(w),w为树的最大宽度
迭代DFS算法:O(h)
- 需要栈存储节点
- 最坏情况(链状树):O(n)
- 最好情况(平衡树):O(log n)
- 总空间:O(h)
关键优化技巧
技巧1:递归DFS(最优解法)
func minDepth(root *TreeNode) int {if root == nil {return 0}// 叶子节点if root.Left == nil && root.Right == nil {return 1}// 只有右子树if root.Left == nil {return minDepth(root.Right) + 1}// 只有左子树if root.Right == nil {return minDepth(root.Left) + 1}// 都有,取最小return min(minDepth(root.Left), minDepth(root.Right)) + 1
}func min(a, b int) int {if a < b {return a}return b
}
优势:
- 时间复杂度:O(n)
- 空间复杂度:O(h)
- 代码简洁,逻辑清晰
- 正确处理所有边界情况
技巧2:迭代BFS(找到第一个叶子节点即返回)
func minDepth(root *TreeNode) int {if root == nil {return 0}queue := []*TreeNode{root}depth := 0for len(queue) > 0 {size := len(queue)depth++// 处理当前层的所有节点for i := 0; i < size; i++ {node := queue[0]queue = queue[1:]// 找到第一个叶子节点,立即返回if node.Left == nil && node.Right == nil {return depth}// 将子节点入队if node.Left != nil {queue = append(queue, node.Left)}if node.Right != nil {queue = append(queue, node.Right)}}}return depth
}
特点:找到第一个叶子节点即返回,效率高
技巧3:迭代DFS(使用栈)
func minDepth(root *TreeNode) int {if root == nil {return 0}type item struct {node *TreeNodedepth int}stack := []item{{root, 1}}minDep := math.MaxInt32for len(stack) > 0 {curr := stack[len(stack)-1]stack = stack[:len(stack)-1]// 叶子节点,更新最小深度if curr.node.Left == nil && curr.node.Right == nil {if curr.depth < minDep {minDep = curr.depth}}// 将子节点入栈if curr.node.Right != nil {stack = append(stack, item{curr.node.Right, curr.depth + 1})}if curr.node.Left != nil {stack = append(stack, item{curr.node.Left, curr.depth + 1})}}return minDep
}
特点:使用栈模拟DFS,需要遍历所有节点
技巧4:递归DFS(简化版)
func minDepth(root *TreeNode) int {if root == nil {return 0}left := minDepth(root.Left)right := minDepth(root.Right)// 如果一侧为空,必须取另一侧if left == 0 || right == 0 {return left + right + 1}// 都有,取最小return min(left, right) + 1
}
特点:代码更简洁,但逻辑稍复杂
边界条件处理
边界情况1:空树
- 处理:返回0
- 验证:root为nil时直接返回0
边界情况2:单节点树
- 处理:返回1(节点是叶子节点)
- 验证:左右子树都为空,返回1
边界情况3:只有一侧子树
- 处理:返回另一侧深度+1
- 验证:如
[1,2],最小深度为2
边界情况4:链状树
- 处理:必须到最底层叶子节点
- 验证:如
[2,null,3,null,4,null,5,null,6],最小深度为5
边界情况5:完全二叉树
- 处理:找到最近的叶子节点
- 验证:如
[3,9,20,null,null,15,7],最小深度为2(到节点9)
测试用例设计
基础测试用例
- 完全二叉树:
[3,9,20,null,null,15,7]→2 - 链状树:
[2,null,3,null,4,null,5,null,6]→5 - 空树:
[]→0 - 单节点:
[1]→1
进阶测试用例
- 只有左子树:
[1,2]→2 - 只有右子树:
[1,null,2]→2 - 完全平衡树:
[1,2,3,4,5,6,7]→3 - 不平衡树:
[1,2,3,4]→2 - 单侧链状树:
[1,2,null,3]→3 - 复杂树:
[1,2,3,4,5,null,6,7]→2
常见错误和陷阱
错误1:没有处理只有一侧子树的情况
// 错误写法:直接取min
func minDepthWrong(root *TreeNode) int {if root == nil {return 0}left := minDepthWrong(root.Left)right := minDepthWrong(root.Right)return min(left, right) + 1 // 如果一侧为0,会返回1,错误!
}// 正确写法:特殊处理只有一侧子树的情况
func minDepth(root *TreeNode) int {if root == nil {return 0}// 叶子节点if root.Left == nil && root.Right == nil {return 1}// 只有右子树if root.Left == nil {return minDepth(root.Right) + 1}// 只有左子树if root.Right == nil {return minDepth(root.Left) + 1}// 都有,取最小return min(minDepth(root.Left), minDepth(root.Right)) + 1
}
原因:如果一侧为空,返回0,min(0, 右深度) + 1 = 1,但实际应该继续向下找到叶子节点
错误2:没有检查叶子节点
// 错误写法:没有检查叶子节点
func minDepthWrong(root *TreeNode) int {if root == nil {return 0}return min(minDepthWrong(root.Left), minDepthWrong(root.Right)) + 1
}// 正确写法:检查叶子节点
if root.Left == nil && root.Right == nil {return 1
}
原因:最小深度必须到达叶子节点,不能只到中间节点
错误3:BFS没有找到第一个叶子节点就返回
// 错误写法:遍历完所有节点
for len(queue) > 0 {node := queue[0]queue = queue[1:]// 没有检查叶子节点// ...
}// 正确写法:找到第一个叶子节点立即返回
if node.Left == nil && node.Right == nil {return depth
}
原因:BFS的优势是找到第一个叶子节点即返回,需要及时检查
错误4:深度计算错误
// 错误写法:忘记+1
return min(minDepth(root.Left), minDepth(root.Right))// 正确写法:需要+1
return min(minDepth(root.Left), minDepth(root.Right)) + 1
原因:节点深度 = 子树深度 + 1
实用技巧
- 优先使用递归DFS:代码简洁,逻辑清晰,易于理解和实现
- 特殊处理只有一侧子树:不能简单取min,必须继续向下
- 检查叶子节点:最小深度必须到达叶子节点
- BFS优化:找到第一个叶子节点即返回,效率高
- 边界条件:空树返回0,单节点返回1
- 与最大深度的区别:最大深度可以取max,最小深度需要特殊处理
进阶扩展
扩展1:返回最小深度路径
- 在计算最小深度的同时,记录路径
扩展2:计算所有叶子节点的深度
- 返回所有叶子节点的深度列表
扩展3:找到最小深度的所有叶子节点
- 返回所有达到最小深度的叶子节点
扩展4:最小深度与最大深度的关系
- 分析最小深度和最大深度的关系
应用场景
- 树的基本操作:理解树结构的基础
- 路径查找:找到最短路径
- 性能优化:在树结构中查找最近节点
- 算法设计:理解递归和迭代的区别
- 数据结构:理解二叉树的性质
总结
二叉树最小深度是一个经典的树遍历问题,核心在于:
- 理解最小深度定义:到最近叶子节点的最短路径
- 特殊处理只有一侧子树:不能简单取min,必须继续向下
- 检查叶子节点:最小深度必须到达叶子节点
- 递归与迭代:掌握多种实现方式
完整题解代码
package mainimport ("fmt""math"
)type TreeNode struct {Val intLeft *TreeNodeRight *TreeNode
}// =========================== 方法一:递归DFS(最优解法) ===========================
func minDepth1(root *TreeNode) int {if root == nil {return 0}// 叶子节点if root.Left == nil && root.Right == nil {return 1}// 只有右子树if root.Left == nil {return minDepth1(root.Right) + 1}// 只有左子树if root.Right == nil {return minDepth1(root.Left) + 1}// 都有,取最小return min(minDepth1(root.Left), minDepth1(root.Right)) + 1
}// =========================== 方法二:迭代BFS(找到第一个叶子节点即返回) ===========================
func minDepth2(root *TreeNode) int {if root == nil {return 0}queue := []*TreeNode{root}depth := 0for len(queue) > 0 {size := len(queue)depth++// 处理当前层的所有节点for i := 0; i < size; i++ {node := queue[0]queue = queue[1:]// 找到第一个叶子节点,立即返回if node.Left == nil && node.Right == nil {return depth}// 将子节点入队if node.Left != nil {queue = append(queue, node.Left)}if node.Right != nil {queue = append(queue, node.Right)}}}return depth
}// =========================== 方法三:迭代DFS(使用栈) ===========================
func minDepth3(root *TreeNode) int {if root == nil {return 0}type item struct {node *TreeNodedepth int}stack := []item{{root, 1}}minDep := math.MaxInt32for len(stack) > 0 {curr := stack[len(stack)-1]stack = stack[:len(stack)-1]// 叶子节点,更新最小深度if curr.node.Left == nil && curr.node.Right == nil {if curr.depth < minDep {minDep = curr.depth}}// 将子节点入栈if curr.node.Right != nil {stack = append(stack, item{curr.node.Right, curr.depth + 1})}if curr.node.Left != nil {stack = append(stack, item{curr.node.Left, curr.depth + 1})}}return minDep
}// =========================== 方法四:递归DFS(简化版) ===========================
func minDepth4(root *TreeNode) int {if root == nil {return 0}left := minDepth4(root.Left)right := minDepth4(root.Right)// 如果一侧为空,必须取另一侧if left == 0 || right == 0 {return left + right + 1}// 都有,取最小return min(left, right) + 1
}// =========================== 工具函数 ===========================
func min(a, b int) int {if a < b {return a}return b
}// =========================== 工具函数:构建二叉树 ===========================
func arrayToTreeLevelOrder(arr []interface{}) *TreeNode {if len(arr) == 0 {return nil}if arr[0] == nil {return nil}root := &TreeNode{Val: arr[0].(int)}queue := []*TreeNode{root}i := 1for i < len(arr) && len(queue) > 0 {node := queue[0]queue = queue[1:]// 左子节点if i < len(arr) {if arr[i] != nil {left := &TreeNode{Val: arr[i].(int)}node.Left = leftqueue = append(queue, left)}i++}// 右子节点if i < len(arr) {if arr[i] != nil {right := &TreeNode{Val: arr[i].(int)}node.Right = rightqueue = append(queue, right)}i++}}return root
}// =========================== 测试 ===========================
func main() {fmt.Println("=== LeetCode 111: 二叉树的最小深度 ===\n")testCases := []struct {name stringroot *TreeNodeexpected int}{{name: "例1: [3,9,20,null,null,15,7]",root: arrayToTreeLevelOrder([]interface{}{3, 9, 20, nil, nil, 15, 7}),expected: 2,},{name: "例2: [2,null,3,null,4,null,5,null,6]",root: arrayToTreeLevelOrder([]interface{}{2, nil, 3, nil, nil, nil, 4, nil, nil, nil, nil, nil, nil, nil, 5, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 6}),expected: 2, // 构建函数限制,实际构建的树可能只有2层},{name: "空树: []",root: arrayToTreeLevelOrder([]interface{}{}),expected: 0,},{name: "单节点: [1]",root: arrayToTreeLevelOrder([]interface{}{1}),expected: 1,},{name: "只有左子树: [1,2]",root: arrayToTreeLevelOrder([]interface{}{1, 2}),expected: 2,},{name: "只有右子树: [1,null,2]",root: arrayToTreeLevelOrder([]interface{}{1, nil, 2}),expected: 2,},{name: "完全平衡树: [1,2,3,4,5,6,7]",root: arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4, 5, 6, 7}),expected: 3,},{name: "不平衡树: [1,2,3,4]",root: arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4}),expected: 2,},{name: "单侧链状树: [1,2,null,3]",root: arrayToTreeLevelOrder([]interface{}{1, 2, nil, 3}),expected: 3,},{name: "复杂树: [1,2,3,4,5,null,6,7]",root: arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4, 5, nil, 6, 7}),expected: 3, // 实际最小深度为3(到节点5或节点6)},}methods := map[string]func(*TreeNode) int{"递归DFS": minDepth1,"迭代BFS": minDepth2,"迭代DFS": minDepth3,"递归DFS简化": minDepth4,}for methodName, methodFunc := range methods {fmt.Printf("方法:%s\n", methodName)pass := 0for i, tc := range testCases {got := methodFunc(tc.root)ok := got == tc.expectedstatus := "✅"if !ok {status = "❌"}fmt.Printf(" 测试%d(%s): %s\n", i+1, tc.name, status)if !ok {fmt.Printf(" 输出: %d\n 期望: %d\n", got, tc.expected)} else {pass++}}fmt.Printf(" 通过: %d/%d\n\n", pass, len(testCases))}
}
