DSAPrep
MediumTrees

Count Good Nodes In Binary Tree

Given a binary tree root, a node X in the tree is named good if in the path from root to X there are no nodes with a value greater than X.

Return the number of good nodes in the binary tree.

Example 1

Input: root = [3,1,4,3,null,1,5]
Output: 4
Explanation: Nodes in blue are good. Root Node (3) is always a good node. Node 4 -> (3,4) is the maximum value in the path starting from the root. Node 5 -> (3,4,5) is the maximum value in the path. Node 3 -> (3,1,3) is also good, since 3 is the maximum value seen so far.

Example 2

Input: root = [3,3,null,4,2]
Output: 3
Explanation: Node 2 -> (3, 3, 2) is not good, because 3 is higher than it.

Example 3

Input: root = [1]
Output: 1

Constraints

  • The number of nodes in the binary tree is in the range [1, 10^5].
  • Each node's value is between [-10^4, 10^4].
View original on LeetCode โ†—

Whether a node is โ€œgoodโ€ depends only on the maximum value seen so far along the path from the root โ€” that value can simply be carried down as an extra DFS argument, so each node is checked in O(1) without ever re-walking a path.

Brute Force (re-walk each path)

Time O(n^2)Space O(h)

For every node, walk back up to the root (or, equivalently, track the full path down and re-scan it) to check whether any ancestor has a strictly greater value.

class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def goodNodes(self, root: TreeNode) -> int:
def count(node: TreeNode | None, path: list[int]) -> int:
if node is None:
return 0
is_good = 1 if node.val >= max(path, default=node.val) else 0
new_path = path + [node.val]
return is_good + count(node.left, new_path) + count(node.right, new_path)
return count(root, [])

Complexity: max(path) re-scans the whole path-so-far at every node, and paths can be O(n) long โ†’ O(n^2) time worst case (a skewed tree). Space for the path list plus recursion stack โ†’ O(h).

DFS Carrying Running Max

OptimalTime O(n)Space O(h)

Pass the maximum value seen so far as a single running argument instead of the whole path. A node is good exactly when its value is at least that running max; the max passed to its children is then max(running_max, node.val).

class Solution:
def goodNodes(self, root: TreeNode) -> int:
def dfs(node: TreeNode | None, max_so_far: int) -> int:
if node is None:
return 0
is_good = 1 if node.val >= max_so_far else 0
new_max = max(max_so_far, node.val)
return is_good + dfs(node.left, new_max) + dfs(node.right, new_max)
return dfs(root, root.val)

Tracing root = [3,1,4,3,null,1,5]:

3
1
4
3
1
5
1 / 5
seenresultdiscarded

Root 3 starts with max_so_far=3. 3 >= 3, so it is good. Pass max_so_far=3 down.

Complexity: each node does O(1) work with the running max instead of rescanning a path โ†’ O(n) time. Space is the recursion stack (the running max is a single O(1) value passed along it) โ†’ O(h).