题目
给你一棵根为 root 的二叉树,请你返回二叉树中好节点的数目。
「好节点」X 定义为:从根到该节点 X 所经过的节点中,没有任何节点的值大于 X 的值。
一、代码实现
func goodNodes(root *TreeNode) int {
if root == nil {
return 0
}
return dfs(root, root.Val)
}
func dfs(node *TreeNode, maxVal int) int {
if node == nil {
return 0
}
count := 0
if node.Val >= maxVal {
count = 1
maxVal = node.Val
}
count += dfs(node.Left, maxVal)
count += dfs(node.Right, maxVal)
return count
}
二、算法分析
1. 核心思路
- 深度优先遍历:通过前序遍历访问每个节点,实时维护路径最大值
- 贪心比较:当前节点值若大于等于路径最大值,则标记为好节点并更新最大值
- 递归分治:将问题分解为左右子树的子问题,合并结果得到总数
2. 关键步骤
- 初始化最大值:以根节点值作为初始路径最大值
- 递归终止条件:空节点返回0
- 节点判断:比较当前节点值与路径最大值,更新计数器和最大值
- 递归分解:分别处理左右子树,传递更新后的最大值
3. 复杂度
指标 | 值 | 说明 |
---|---|---|
时间复杂度 | O(n) | 每个节点访问一次 |
空间复杂度 | O(h) | h为树的高度(递归栈空间) |
三、图解示例
以二叉树[3,1,4,3,null,1,5]
为例:
3
/ \
1 4
/ / \
3 1 5
递归过程:
- 根节点3:路径最大3 → 好节点(计数1)
- 左子节点1:路径最大3 → 不计数
- 左子节点的左子3:路径最大3 → 好节点(计数+1)
- 右子节点4:路径最大4 → 好节点(计数+1)
- 右子节点的左子1:路径最大4 → 不计数
- 右子节点的右子5:路径最大5 → 好节点(计数+1)
总计数:1 + 1 + 1 + 1 = 4
四、边界条件与扩展
1. 特殊场景验证
- 单节点树:返回1
- 递减序列:如
5→4→3→2
→ 返回4(每个节点都是好节点) - 负数值:如
[-2,null,-3]
→ 返回1(仅根节点是好节点)
2. 多语言实现
class Solution:
def goodNodes(self, root: TreeNode) -> int:
def dfs(node, max_val):
if not node: return 0
count = 0
if node.val >= max_val:
count = 1
max_val = node.val
return count + dfs(node.left, max_val) + dfs(node.right, max_val)
return dfs(root, root.val)
class Solution {
public int goodNodes(TreeNode root) {
return dfs(root, root.val);
}
private int dfs(TreeNode node, int maxVal) {
if (node == null) return 0;
int count = 0;
if (node.val >= maxVal) {
count = 1;
maxVal = node.val;
}
return count + dfs(node.left, maxVal) + dfs(node.right, maxVal);
}
}
五、总结与扩展
1. 核心创新点
- 路径最大值传递:通过递归参数动态维护路径最大值
- 高效计数机制:仅需单次遍历即可完成所有判断
- 空间优化:利用递归栈替代显式栈结构
2. 扩展应用
- 路径最大值统计:可扩展记录所有路径中的最大值分布
- 节点标记存储:修改算法以存储所有好节点列表
- 多条件筛选:结合其他条件(如最小值、奇偶性)扩展筛选逻辑
3. 工程优化方向
- 迭代实现:用栈模拟递归过程避免栈溢出
- 并行计算:对左右子树进行并发遍历
- 缓存优化:对大规模数据预计算路径特征