1448 - Count Good Nodes in Binary Tree
Difficulty: Medium | Pattern: Tree DFS | Company tags: Microsoft, Amazon, Facebook
Problem Statement
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.val.
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 3(root), 4, 3, 5 are good.
3
/ \
1 4
/ / \
3 1 5
Example 2:
Input: root = [3,3,null,4,2]
Output: 3
Approach: DFS with Max Tracking — O(n)
Key insight: DFS from root, tracking the maximum value seen so far on the current path. A node is "good" if its value is >= that maximum.
def goodNodes(root) -> int:
def dfs(node, max_so_far):
if not node:
return 0
count = 1 if node.val >= max_so_far else 0
new_max = max(max_so_far, node.val)
count += dfs(node.left, new_max)
count += dfs(node.right, new_max)
return count
return dfs(root, float('-inf'))
Algorithm Flow
Dry Run
Tree: [3,1,4,3,null,1,5]
dfs(3, -inf): 3 >= -inf ✓ count=1, max=3
dfs(1, 3): 1 < 3 ✗ count=0, max=3
dfs(3, 3): 3 >= 3 ✓ count=1 → returns 1
dfs(None, 3): 0
→ returns 1
dfs(4, 3): 4 >= 3 ✓ count=1, max=4
dfs(1, 4): 1 < 4 ✗ count=0 → returns 0
dfs(5, 4): 5 >= 4 ✓ count=1 → returns 1
→ returns 0+1+1=2
Total: 1+1+2 = 4
Output: 4 ✓
Iterative BFS Version
from collections import deque
def goodNodes(root) -> int:
if not root:
return 0
count = 0
queue = deque([(root, float('-inf'))])
while queue:
node, max_so_far = queue.popleft()
if node.val >= max_so_far:
count += 1
new_max = max(max_so_far, node.val)
if node.left:
queue.append((node.left, new_max))
if node.right:
queue.append((node.right, new_max))
return count
Edge Cases
- Single node → always good (root is always good)
- All decreasing values down → only root is good
- All same values → all nodes are good
Complexity
- Time: O(n) — visit every node once
- Space: O(h) — recursion stack; O(log n) balanced, O(n) skewed
Key Terms
| Term | Definition |
|---|---|
| DFS (Depth-First Search) | Traversal strategy that fully explores one branch of the tree before backtracking, used here to walk root-to-leaf paths. |
| Path maximum tracking | Carrying the running maximum value seen along the current root-to-node path as a parameter through recursion. |
| Good node | A node whose value is greater than or equal to every ancestor on its path from the root. |
| Recursion stack | The implicit call stack used by DFS; its depth equals the tree height and determines auxiliary space usage. |
| Root-to-node path | The unique sequence of nodes from the root down to a given node in a tree, used as the scope for the "no greater ancestor" condition. |
FAQ
-
Can this be solved without extra space (excluding recursion)? The DFS approach uses O(1) extra variables per call; the only space cost is the O(h) recursion stack, which is unavoidable for tree traversal unless converted to an iterative approach with an explicit stack, which still costs O(h).
-
What if the tree is empty?
rootisNone, sodfsreturns0immediately (or the BFS loop never starts), and the answer is0good nodes. -
How would this change if we wanted the actual list of good nodes instead of a count? Instead of returning an integer count, append
node.val(or the node itself) to a shared list or accumulate results in a return list from each recursive call, merging left and right results plus the current node when it qualifies. -
Why pass
max_so_faras a parameter instead of using a global/mutable variable? Each root-to-node path has its own independent maximum; passing it by value keeps sibling subtrees isolated so a high value in the left subtree doesn't incorrectly affect the right subtree's evaluation. -
Does the initial value need to be
float('-inf'), or could it beroot.val? Usingfloat('-inf')guarantees the root itself is always counted as good regardless of its value (including negative values), which matches the problem's definition that the root always qualifies.
Quick Revision
- Problem: count nodes where no ancestor on the root-to-node path has a strictly greater value.
- Pattern: single-pass DFS carrying the max value seen so far as a parameter.
- A node is good if
node.val >= max_so_far. - Update
max_so_fartomax(max_so_far, node.val)before recursing into children. - Base case:
Nonenode contributes0to the count. - Initialize the top-level call with
max_so_far = float('-inf')so the root is always good. - Sum counts from left subtree, right subtree, and current node's own contribution.
- Time complexity O(n) since every node is visited exactly once.
- Space complexity O(h): O(log n) for balanced trees, O(n) worst case for skewed trees.
- Iterative BFS variant swaps the call stack for an explicit queue holding
(node, max_so_far)pairs.
Related Problems
- 104-MaximumDepthOfBinaryTree.md — same DFS-with-recursion pattern for computing a value along tree paths.
- 112-PathSum.md — DFS carrying a running value (remaining sum) down each root-to-leaf path, analogous to carrying
max_so_far. - 113-PathSumII.md — extends path tracking to collect and return full paths rather than just a count.