题目描述

✅ 1448. 统计二叉树中好节点的数目

image-20260928234020600

image-20260928234020602

题意分析

一个节点是好节点,当且仅当从根到它的整条路径上,没有任何节点值比它更大。需要统计这样的节点总数,路径中的值与当前值相等时仍然符合条件。

比较范围仅是当前节点自己的祖先路径,不包括其他分支。根节点没有祖先,因此必然是好节点;节点值可能为负,不能把路径最大值默认成零。

解法:DFS 下传当前祖先最大值

核心思路

[!blue]

判断当前节点时,不需要重新扫描整条祖先路径,只需知道祖先里的最大值。定义递归参数 pathMax 为从根到当前节点父亲的最大值;若 node.val >= pathMax,就表示不存在更大的祖先,当前节点贡献一个好节点。

继续处理孩子前,计算 newMax = max(pathMax, node.val),使传下去的状态包含当前节点。这样每个子调用收到的仍是它全部祖先的最大值,同一个状态定义可以逐层重复使用。

左右子树都应从相同的父路径状态开始。newMax 是按值传递的整数,左子树内部产生的更大值只影响左边后代,不会改动右子树收到的参数,因此无需额外回溯恢复,也不能用全树共享的最大值替代它。

当前节点不是好节点,也不能停止搜索:后代仍可能出现足够大的值,重新达到祖先最大值。因此每个非空节点都要递归两个孩子,把当前贡献与左右子树返回的计数相加。空节点返回零;根使用小于题目所有节点值的哨兵,统一处理没有祖先的情况。

解题步骤

  1. 从根开始递归,将最小整数作为初始祖先最大值。
  2. 当前节点为空时返回零;否则比较当前值与 pathMax,决定本节点贡献零还是一。
  3. 计算包含当前值的新路径最大值,分别传给左右孩子。
  4. 将左右子树计数和当前贡献相加并返回,最外层得到全树答案。

代码实现

class Solution {
    public int goodNodes(TreeNode root) {
        // 根节点没有祖先,用比任何节点值都小的哨兵作初始路径最大值,保证根一定被计入
        return dfs(root, Integer.MIN_VALUE);
    }

    // pathMax:根到 node 父节点这条路径上的最大值(不变量)
    private int dfs(TreeNode node, int pathMax) {
        if (node == null) {
            // 空节点不是节点,不参与计数
            return 0;
        }

        // 「路径上不存在更大的值」等价于当前值不小于路径最大值,等号必须保留
        int count = node.val >= pathMax ? 1 : 0;
        // 下传前更新为根到当前节点的最大值
        int newMax = Math.max(pathMax, node.val);

        // 两棵子树共用同一个 newMax,互不干扰
        count += dfs(node.left, newMax);
        count += dfs(node.right, newMax);

        // 返回以 node 为根的子树中好节点的数目
        return count;
    }
}
func goodNodes(root *TreeNode) int {
    // pathMax:根到 node 父节点这条路径上的最大值(不变量)
    var dfs func(node *TreeNode, pathMax int) int
    dfs = func(node *TreeNode, pathMax int) int {
        if node == nil {
            // 空节点不是节点,不参与计数
            return 0
        }
        count := 0
        // 等号必须保留:路径上有相等值时当前节点仍是好节点
        if node.Val >= pathMax {
            count = 1
        }
        newMax := pathMax
        if node.Val > newMax {
            // 下传前更新为根到当前节点的最大值
            newMax = node.Val
        }
        // 两棵子树共用同一个 newMax,互不干扰
        count += dfs(node.Left, newMax)
        count += dfs(node.Right, newMax)
        // 返回以 node 为根的子树中好节点的数目
        return count
    }
    // 根节点没有祖先,用比任何节点值都小的哨兵作初始路径最大值
    return dfs(root, -1<<31)
}

复杂度分析

  • 时间复杂度:$O(n)$,每个节点访问一次,判断与更新最大值都是常数操作。
  • 空间复杂度:$O(h)$,h 为树高,来自递归调用栈;平衡树为 $O(\log n)$,退化为链时为 $O(n)$。

关键点总结

[!green]

  • 祖先路径的所有比较可以压缩成一个最大值,不必重复扫描路径。
  • 当前判断使用祖先最大值,下传给孩子前再包含当前节点。
  • 左右分支各自持有路径状态,避免跨分支污染。
  • 当前不合格不代表整棵子树都不合格,仍需遍历后代。

易错点总结

[!yellow]

  • 使用严格大于判断,会漏掉与祖先最大值相等的好节点。
  • 用全树最大值或遍历至今最大值判断,会让其他分支错误限制当前路径。
  • 初始最大值设为零,会误判负数根以及只包含负值的路径。
  • 传给孩子时没有纳入当前节点,会遗漏当前节点对后代的限制。
  • 遇到坏节点就剪枝,会丢掉它下面可能出现的好节点。

相似题目

题目 难度 关联与区别
112. 路径总和 简单 同样DFS携带从根到当前位置的路径状态,原题携带剩余目标和,本题携带路径最大值。
1026. 节点与其祖先之间的最大差值 中等 同样沿根路径携带极值,原题同时保留最大、最小值计算祖先差。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/40512884
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!