Skip to content

1448. Count Good Nodes in Binary Tree (Medium)

Problem

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

Example

  • root = [3,1,4,3,null,1,5]4 (roots 3, 4, 5, and the left subtree’s 3)
  • root = [3,3,null,4,2]3
  • root = [1]1

LeetCode 1448 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, for each node, walk up to the root

For every node, walk its ancestor chain to verify the invariant.

def good_nodes(root):
from collections import deque
parent = {root: None} # L1: O(1) init
q = deque([root])
while q:
n = q.popleft() # L2: O(1) dequeue
for child in (n.left, n.right):
if child:
parent[child] = n # L3: O(1) record parent
q.append(child)
count = 0
for node in parent:
is_good = True
cur, v = parent[node], node.val
while cur is not None:
if cur.val > v: # L4: O(h) ancestor walk per node
is_good = False
break
cur = parent[cur]
if is_good:
count += 1
return count

Where the time goes, line by line

Variables: n = number of nodes in the tree, h = tree height.

LinePer-call costTimes executedContribution
L2/L3 (BFS build)O(1)O(1)nO(n)O(n)
L4 (ancestor walk)O(h)O(h)nO(nh)O(n · h) ← dominates

Each of the n nodes walks up to O(h)O(h) ancestors. In a balanced tree this is O(nlogn)O(n log n); in a skewed tree it’s O(n2)O(n²).

Complexity

  • Time: O(nh)O(n · h). Each of n nodes walks up O(h)O(h).
  • Space: O(n)O(n) for the parent map.

Wasteful, we’re re-walking ancestor chains that largely overlap.

Approach 2: DFS carrying running max (optimal)

Walk top-down with the running maximum seen so far on the root-to-current path. A node is good iff node.val >= running_max.

def good_nodes(root):
def dfs(node, max_so_far):
if not node:
return 0
good = 1 if node.val >= max_so_far else 0 # L1: O(1) check
new_max = max(max_so_far, node.val) # L2: O(1) update
return good + dfs(node.left, new_max) + dfs(node.right, new_max) # L3: recurse
return dfs(root, float('-inf'))

Where the time goes, line by line

Variables: n = number of nodes in the tree, h = tree height.

LinePer-call costTimes executedContribution
L1 (good check)O(1)O(1)nO(n)O(n)
L2 (update max)O(1)O(1)nO(n)O(n)
L3 (recurse both)O(1)O(1) dispatchnO(n)O(n) ← dominates (all lines tie)

Each node is visited exactly once with O(1)O(1) work, carrying the running max as a parameter.

Complexity

  • Time: O(n)O(n), driven by L3 visiting every node once.
  • Space: O(h)O(h) recursion.

Pattern: “path-state DFS”

Carry a running invariant (max, min, sum, seen-set) as a parameter. Works for every “a node is X iff the root-to-node path is Y” problem.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: Iterative DFS with explicit path state

Stack of (node, max_so_far) pairs.

def good_nodes(root):
if not root:
return 0
stack = [(root, float('-inf'))] # L1: O(1) init
count = 0
while stack:
node, mx = stack.pop() # L2: O(1) pop
if node.val >= mx:
count += 1 # L3: O(1) count
new_max = max(mx, node.val) # L4: O(1) update
if node.left:
stack.append((node.left, new_max)) # L5: O(1) push
if node.right:
stack.append((node.right, new_max)) # L6: O(1) push
return count

Where the time goes, line by line

Variables: n = number of nodes in the tree, h = tree height.

LinePer-call costTimes executedContribution
L2 (pop)O(1)O(1)nO(n)O(n)
L3/L4 (check + update)O(1)O(1)nO(n)O(n)
L5/L6 (push children)O(1)O(1)nO(n)O(n) ← dominates (all lines tie)

Complexity

  • Time: O(n)O(n).
  • Space: O(h)O(h).

Summary

ApproachTimeSpace
Ancestor walksO(nh)O(n · h)O(n)O(n)
DFS with running maxO(n)O(n)O(h)O(h)
Iterative DFS with stateO(n)O(n)O(h)O(h)

The recursive path-state DFS is the canonical answer and the template for many “longest increasing path” / “valid-along-path” tree problems.

Test cases

class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(vals):
if not vals: return None
root = TreeNode(vals[0])
q = [root]
i = 1
while q and i < len(vals):
node = q.pop(0)
if i < len(vals) and vals[i] is not None:
node.left = TreeNode(vals[i])
q.append(node.left)
i += 1
if i < len(vals) and vals[i] is not None:
node.right = TreeNode(vals[i])
q.append(node.right)
i += 1
return root
def good_nodes(root):
def dfs(node, max_so_far):
if not node:
return 0
good = 1 if node.val >= max_so_far else 0
new_max = max(max_so_far, node.val)
return good + dfs(node.left, new_max) + dfs(node.right, new_max)
return dfs(root, float('-inf'))
def _run_tests():
assert good_nodes(build_tree([3, 1, 4, 3, None, 1, 5])) == 4
assert good_nodes(build_tree([3, 3, None, 4, 2])) == 3
assert good_nodes(build_tree([1])) == 1
# 5 (good), 6 (good, >=5), 7 (good, >=6); 4 and 3 not good
assert good_nodes(build_tree([5, 4, 6, 3, None, None, 7])) == 3
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Tree Traversal, the recursive or iterative visit pattern for carrying path and subtree state.
  • DFS, the depth first traversal habit of following one branch before returning.