题目描述

✅ 104. 二叉树的最大深度

image-20260928195050699

image-20260928195050700

题意分析

最大深度是从根节点到最远叶子节点的路径所包含的节点数量。空树深度为 0,只有根节点时深度为 1;计算的是节点数,不是边数。

一条从根向下的路径每次只能选择左、右子树中的一侧,不能同时经过两边。因此只需分别求出左右子树的最大深度,取较大者,再加上当前根节点这一层。下面先给出递归写法,再说明通过临时连接实现常数额外空间的 Morris 遍历。

解法:递归计算左右子树深度

核心思路

[!blue]

先约定递归函数 maxDepth(root) 返回“以当前节点为根的子树有多高”,而不是从整棵树的根走到当前位置已经经过多少层。返回值只由当前子树决定,父节点可以直接使用它。

当前节点为空时,这棵子树没有节点,返回 0。否则分别递归得到左子树高度 leftDepth 和右子树高度 rightDepth。从当前节点出发的最长向下路径,必然选择两者中更深的一侧,再经过当前节点,所以返回 max(leftDepth, rightDepth) + 1。

为什么取最大值而不是相加?两边相加会把左右两条不同路径拼到一起,而根到叶子的路径只能往一侧继续。只取其中一侧又可能漏掉另一边更深的叶子,所以两个子树都必须计算。

叶子的两个孩子都为空,按这个规则自然得到 1;只有一个孩子时,空侧贡献 0,较深的真实子树会被选中,无需额外分支。每层等待孩子先返回结果再计算自身,最终根节点收到的值就是整棵树的最大深度。

解题步骤

  1. 当前节点为空时返回 0。
  2. 递归计算左子树与右子树的最大深度。
  3. 取两个结果中的较大值并加 1,计入当前节点这一层。
  4. 从整棵树的根调用,返回值即为答案。

代码实现

class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }

        int leftDepth = maxDepth(root.left);
        int rightDepth = maxDepth(root.right);

        // 取较深的一侧,再计入当前根节点这一层。
        return Math.max(leftDepth, rightDepth) + 1;
    }
}
func maxDepth(root *TreeNode) int {
    if root == nil {
        return 0
    }
    leftDepth := maxDepth(root.Left)
    rightDepth := maxDepth(root.Right)
    // 取较深的一侧,再计入当前根节点这一层。
    if leftDepth > rightDepth {
        return leftDepth + 1
    }
    return rightDepth + 1
}

复杂度分析

  • 时间复杂度:$O(n)$,每个节点访问一次。
  • 空间复杂度:$O(h)$,递归栈深度等于树高 h;树退化成链时最坏为 $O(n)$,完全平衡时为 $O(\log n)$。

关键点总结

[!green]

  • 递归函数返回当前子树的最大深度。
  • 空节点返回 0,叶子节点自然得到 1。
  • 左右子树都要计算,最长路径可能位于任意一侧。

解法二:Morris 遍历

核心思路

[!blue]

递归需要调用栈保存“左子树处理完后回到哪里”。Morris 遍历利用原树中本来为空的右指针临时保存返回路线,从而不用显式栈。它适合允许遍历期间临时修改指针、又要求常数额外空间的情况;遍历结束必须恢复原树。

对当前节点 cur,如果有左子树,就从 cur.left 沿右指针找到最右节点 pred。它是当前节点的中序前驱。第一次到达 cur 时,pred.right 为空,将其临时设为 cur,然后进入左子树。左子树处理完后会沿这条线索回到 cur,再次寻找前驱时就会发现 pred.right == cur,据此区分首次到达和返回。

depth 需要与这两种到达方式分别配合。第一次到达一个节点之前,它保存的是这个节点上一层的深度,因此先加一,就得到当前节点的真实深度,并用它更新 best。没有左孩子时无需建立线索,记录后直接沿右指针继续;有左孩子且首次到达时,记录后建立线索并向左走。

沿线索返回时,depth 仍是前驱 pred 的真实深度,不能把这次回到 cur 当成又向下走了一层。寻找前驱时用 steps 记录从 cur 先向左、再沿右边界到达 pred 的边数:先走到左孩子算一步,之后每向右移动一次再加一。

因此前驱比当前节点深 steps 层。第二次到达 cur 时,先把 pred.right 恢复为空,再执行 depth -= steps,就把深度恢复到 cur 所在层,然后进入原右子树。右子树节点首次被处理时再加一,深度衔接便与真正的树路径一致。

只有首次到达时才更新 best,临时返回路线不参与最大深度计算。每个节点都会在首次到达时记录自己的真实深度,因此最大值不会遗漏;同时每条建立的线索都会在第二次到达对应节点时被拆除,完整遍历后原树结构恢复。

解题步骤

  1. 初始化 cur = root、depth = 0、best = 0。
  2. 若 cur 没有左孩子,将 depth 加一并更新 best,然后沿右指针继续。
  3. 若有左孩子,寻找中序前驱 pred,同时用 steps 统计从 cur 到它的左转后右行路径长度;遇到空右指针或指回 cur 的线索就停止。
  4. 若 pred.right 为空,说明首次到达:增加并记录深度,建立 pred.right = cur,再进入左子树。
  5. 否则说明左子树已处理完:拆除线索,执行 depth -= steps,再进入右子树,不重复增加深度。
  6. 直到 cur 为空,返回 best;空树不进入循环,返回 0。

代码实现

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int val) {
        this.val = val;
    }
}

class Solution {
    public int maxDepth(TreeNode root) {
        int depth = 0;
        int best = 0;
        TreeNode cur = root;

        while (cur != null) {
            if (cur.left == null) {
                depth++;
                best = Math.max(best, depth);
                cur = cur.right;
            } else {
                TreeNode pred = cur.left;
                int steps = 1;

                while (pred.right != null && pred.right != cur) {
                    pred = pred.right;
                    steps++;
                }

                if (pred.right == null) {
                    depth++;
                    best = Math.max(best, depth);
                    pred.right = cur;
                    cur = cur.left;
                } else {
                    pred.right = null;
                    depth -= steps;
                    cur = cur.right;
                }
            }
        }

        return best;
    }
}
type TreeNode struct {
    Val         int
    Left, Right *TreeNode
}

func maxDepth(root *TreeNode) int {
    depth, best := 0, 0
    cur := root
    for cur != nil {
        if cur.Left == nil {
            depth++
            best = max(best, depth)
            cur = cur.Right
        } else {
            pred := cur.Left
            steps := 1
            for pred.Right != nil && pred.Right != cur {
                pred = pred.Right
                steps++
            }
            if pred.Right == nil {
                depth++
                best = max(best, depth)
                pred.Right = cur
                cur = cur.Left
            } else {
                pred.Right = nil
                depth -= steps
                cur = cur.Right
            }
        }
    }
    return best
}

复杂度分析

  • 时间复杂度:$O(n)$,节点只会首次到达或沿线索返回,寻找前驱的各条右边界也只被常数次扫描,嵌套循环的总工作量为线性。
  • 空间复杂度:$O(1)$,只使用固定数量的指针和计数器,临时线索占用原节点中本来为空的指针位置。

关键点总结

[!green]

  • 两次到达含义不同:首次到达计入当前层并更新答案,沿线索返回只恢复深度,不重复计层。
  • steps 是真实路径边数:初始为 1,已经计入从当前节点到左孩子的那一步。
  • 线索必须恢复:拆除时令 pred.right = null,不能在仍有临时连接时提前结束,否则会改变原树。

易错点总结

[!yellow]

  • 忘记空节点判断会触发空指针错误。
  • 返回值漏掉 +1,会少算当前节点这一层。
  • 只递归一侧会漏掉另一侧的更深路径。
  • 最大深度按节点数计算,单节点树的深度是 1。

相似题目

题目 难度 关联与区别
111. 二叉树的最小深度 简单 最大深度在两侧取最大值,最小深度必须走到真实叶子,不能把缺失孩子当零深度路径。
110. 平衡二叉树 简单 复用后序求高度,并在每个节点检查左右高度差。
543. 二叉树的直径 简单 后序返回子树高度并在根处合并信息;本题取两侧最大高度加一,该题合并左右向下路径得到直径。
124. 二叉树中的最大路径和 困难 后序返回子树高度并在根处合并信息;本题取两侧最大高度加一,该题舍弃负贡献后合并最大路径和。
687. 最长同值路径 中等 后序返回子树高度并在根处合并信息;本题取两侧最大高度加一,该题仅连接值相同的向下链。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/08982532
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!