1448. Count Good Nodes in Binary Tree
Problem Description
You are given a binary tree with a root node. A node X in the tree is considered good if along the path from the root to node X, there are no nodes with values greater than X's value.
In other words, a node is good if its value is greater than or equal to all the values of its ancestor nodes (including the root) on the path from root to that node.
Your task is to count and return the total number of good nodes in the binary tree.
For example:
- The root node is always good (no ancestors to compare with)
- A node with value 5 is good if all nodes on the path from root to this node have values ≤ 5
- A node with value 3 would not be good if any ancestor node on its path has a value > 3
The solution uses a depth-first search (DFS) approach where it tracks the maximum value seen so far along each path. Starting from the root with an initial maximum of -1000000
(a very small number), it traverses the tree. At each node, if the current node's value is greater than or equal to the maximum value seen so far on that path (mx <= root.val
), the node is counted as good and the maximum is updated. The function recursively checks both left and right subtrees, passing along the updated maximum value for each path.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.
Is it a tree?
- Yes: The problem explicitly states we're working with a binary tree structure with a root node and parent-child relationships.
DFS
- We arrive at DFS: Since we're dealing with a tree structure, the flowchart leads us directly to using Depth-First Search (DFS).
Why DFS is appropriate for this problem:
- We need to traverse from the root to each node while maintaining information about the path (specifically, the maximum value seen so far)
- DFS naturally maintains the path context as we recursively traverse down the tree
- At each node, we need to make a decision based on all ancestors in the current path, which DFS handles perfectly by passing parameters through the recursive calls
- The problem requires visiting every node exactly once to count all good nodes
Conclusion: The flowchart correctly suggests using DFS for this tree traversal problem. The DFS approach allows us to maintain the maximum value encountered along each root-to-node path and efficiently determine whether each node is "good" by comparing it with this tracked maximum.
Intuition
The key insight is that we need to track the maximum value seen along each path from root to the current node. Think of it like walking down a path and keeping track of the highest point you've reached so far.
When we visit a node, we only need to know one piece of information from the path above it: what's the largest value we've encountered so far? If the current node's value is greater than or equal to this maximum, then by definition, no ancestor has a value greater than the current node - making it a "good" node.
This naturally suggests a recursive approach where we:
- Pass down the maximum value seen so far as we traverse
- At each node, check if
current_value >= max_seen_so_far
- If yes, increment our count and update the maximum for the subtrees
- Continue this process for all nodes
Why does this work? Because when we're at any node X, we've already visited all its ancestors (that's how we got to X in the first place). The mx
parameter captures the essential information from all those ancestors - their maximum value. We don't need to remember the entire path or all the ancestor values, just the maximum one.
Starting with a very small initial maximum (like -1000000
) ensures that the root node is always counted as good, which makes sense since the root has no ancestors to be compared against.
The beauty of this approach is its simplicity - we're reducing the entire path history to a single number (the maximum), making the solution both elegant and efficient with O(n)
time complexity and O(h)
space complexity where h is the height of the tree.
Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.
Solution Approach
The implementation uses a recursive DFS helper function to traverse the binary tree while maintaining the maximum value seen along each path.
Core Implementation Structure:
-
Helper Function Definition: We define an inner function
dfs(root, mx)
where:root
is the current node being visitedmx
is the maximum value seen from the root to the current node's parent
-
Base Case: If the current node is
None
, we simply return (nothing to process). -
Good Node Check: For each node, we check if
mx <= root.val
:- If true, the current node is good (no ancestor has a greater value)
- We increment the answer counter using
nonlocal ans
to modify the outer scope variable - We update
mx = root.val
for the recursive calls to children
-
Recursive Traversal: We recursively call DFS on both children:
dfs(root.left, mx)
- traverse left subtree with updated maximumdfs(root.right, mx)
- traverse right subtree with updated maximum
Key Implementation Details:
-
Initial Maximum Value: We start with
mx = -1000000
, a value smaller than any possible node value in the tree. This ensures the root is always counted as good. -
Nonlocal Variable: The
ans
variable is declared in the outer function scope and modified usingnonlocal
keyword. This allows the nested function to accumulate the count across all recursive calls. -
Path Context Preservation: Each recursive call maintains its own
mx
value. When we go left or right from a node, both branches get the same maximum value up to that point, but their subsequent updates don't affect each other.
Algorithm Flow:
- Initialize
ans = 0
to store the count of good nodes - Call
dfs(root, -1000000)
to start traversal from root - At each node:
- Check if it's good (
mx <= root.val
) - Update count and maximum if needed
- Recursively process children
- Check if it's good (
- Return the final count
The time complexity is O(n)
where n is the number of nodes (we visit each node exactly once), and the space complexity is O(h)
where h is the height of the tree (for the recursion call stack).
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the algorithm works.
Consider this binary tree:
3 / \ 1 4 / / \ 3 1 5
We want to count the good nodes. Remember, a node is "good" if no ancestor has a value greater than it.
Step-by-step traversal:
-
Start at root (value 3)
- Current node: 3, Maximum seen so far: -1000000
- Is 3 ≥ -1000000? Yes! → Node 3 is good, count = 1
- Update maximum for children: mx = 3
- Recursively visit left child (1) and right child (4)
-
Visit left child (value 1)
- Current node: 1, Maximum seen so far: 3
- Is 1 ≥ 3? No → Node 1 is not good, count stays 1
- Keep maximum as 3 for its children
- Recursively visit its left child (3)
-
Visit node with value 3 (left-left)
- Current node: 3, Maximum seen so far: 3
- Is 3 ≥ 3? Yes! → Node 3 is good, count = 2
- No children to visit, backtrack
-
Back to root, now visit right child (value 4)
- Current node: 4, Maximum seen so far: 3
- Is 4 ≥ 3? Yes! → Node 4 is good, count = 3
- Update maximum for children: mx = 4
- Recursively visit its children (1 and 5)
-
Visit node with value 1 (right-left)
- Current node: 1, Maximum seen so far: 4
- Is 1 ≥ 4? No → Node 1 is not good, count stays 3
- No children to visit, backtrack
-
Visit node with value 5 (right-right)
- Current node: 5, Maximum seen so far: 4
- Is 5 ≥ 4? Yes! → Node 5 is good, count = 4
- No children to visit, backtrack
Final result: 4 good nodes (values 3, 3, 4, and 5)
The key insight illustrated here: each path maintains its own maximum value. When we visited the left subtree with node 1, we carried mx=3. When we visited the right subtree with node 4, we also started with mx=3. But after visiting node 4, its children received mx=4, which is why node 5 needed to be ≥ 4 to be considered good.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8class Solution:
9 def goodNodes(self, root: TreeNode) -> int:
10 """
11 Count the number of good nodes in a binary tree.
12 A node is considered 'good' if there are no nodes with greater values
13 on the path from root to that node.
14
15 Args:
16 root: The root node of the binary tree
17
18 Returns:
19 The count of good nodes in the tree
20 """
21
22 def dfs(node: TreeNode, max_so_far: int) -> None:
23 """
24 Depth-first search to traverse the tree and count good nodes.
25
26 Args:
27 node: Current node being visited
28 max_so_far: Maximum value encountered on the path from root to current node
29 """
30 # Base case: if node is None, return
31 if node is None:
32 return
33
34 # If current node's value is >= max value seen so far, it's a good node
35 if node.val >= max_so_far:
36 nonlocal good_node_count
37 good_node_count += 1
38 # Update the maximum value for the path
39 max_so_far = node.val
40
41 # Recursively visit left and right subtrees with updated maximum
42 dfs(node.left, max_so_far)
43 dfs(node.right, max_so_far)
44
45 # Initialize counter for good nodes
46 good_node_count = 0
47
48 # Start DFS from root with initial maximum as negative infinity
49 # Using -10^5 as per typical constraints (node values are usually >= -10^4)
50 dfs(root, float('-inf'))
51
52 return good_node_count
53
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 // Counter for the number of good nodes found
18 private int goodNodeCount = 0;
19
20 /**
21 * Counts the number of good nodes in a binary tree.
22 * A node is considered "good" if the path from root to that node
23 * contains no node with a value greater than the node's value.
24 *
25 * @param root The root of the binary tree
26 * @return The total number of good nodes in the tree
27 */
28 public int goodNodes(TreeNode root) {
29 // Start DFS traversal with initial maximum value set to minimum integer
30 depthFirstSearch(root, Integer.MIN_VALUE);
31 return goodNodeCount;
32 }
33
34 /**
35 * Performs depth-first search to count good nodes.
36 *
37 * @param node The current node being visited
38 * @param maxSoFar The maximum value encountered from root to current node's parent
39 */
40 private void depthFirstSearch(TreeNode node, int maxSoFar) {
41 // Base case: if node is null, return
42 if (node == null) {
43 return;
44 }
45
46 // Check if current node is a good node
47 if (maxSoFar <= node.val) {
48 // Increment good node counter
49 goodNodeCount++;
50 // Update maximum value for child nodes
51 maxSoFar = node.val;
52 }
53
54 // Recursively traverse left subtree
55 depthFirstSearch(node.left, maxSoFar);
56 // Recursively traverse right subtree
57 depthFirstSearch(node.right, maxSoFar);
58 }
59}
60
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 /**
15 * Count the number of good nodes in a binary tree.
16 * A node is considered "good" if the path from root to that node
17 * doesn't contain any node with a value greater than the node's value.
18 *
19 * @param root The root of the binary tree
20 * @return The count of good nodes in the tree
21 */
22 int goodNodes(TreeNode* root) {
23 int goodNodeCount = 0;
24
25 // Define a lambda function for depth-first search traversal
26 // Captures goodNodeCount by reference to update the count
27 function<void(TreeNode*, int)> dfs = [&](TreeNode* node, int maxSoFar) {
28 // Base case: if node is null, return
29 if (!node) {
30 return;
31 }
32
33 // Check if current node is a good node
34 // A node is good if its value is >= all values on the path from root
35 if (maxSoFar <= node->val) {
36 ++goodNodeCount;
37 // Update the maximum value seen so far on this path
38 maxSoFar = node->val;
39 }
40
41 // Recursively traverse left subtree with updated maximum
42 dfs(node->left, maxSoFar);
43
44 // Recursively traverse right subtree with updated maximum
45 dfs(node->right, maxSoFar);
46 };
47
48 // Start DFS from root with initial maximum as negative infinity
49 // Using -1e6 as a sufficiently small value (constraint: node values are in range [-10^4, 10^4])
50 dfs(root, -1000000);
51
52 return goodNodeCount;
53 }
54};
55
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15/**
16 * Counts the number of "good" nodes in a binary tree.
17 * A node is considered "good" if the path from root to that node
18 * contains no node with a value greater than the node itself.
19 *
20 * @param root - The root node of the binary tree
21 * @returns The count of good nodes in the tree
22 */
23function goodNodes(root: TreeNode | null): number {
24 // Initialize counter for good nodes
25 let goodNodeCount: number = 0;
26
27 /**
28 * Performs depth-first search to traverse the tree and count good nodes
29 *
30 * @param node - Current node being visited
31 * @param maxValueSoFar - Maximum value encountered from root to current node's parent
32 */
33 const traverseAndCount = (node: TreeNode | null, maxValueSoFar: number): void => {
34 // Base case: if node is null, return
35 if (!node) {
36 return;
37 }
38
39 // Check if current node is a good node
40 // A node is good if its value is greater than or equal to all previous values in the path
41 if (maxValueSoFar <= node.val) {
42 goodNodeCount++;
43 // Update the maximum value for child nodes
44 maxValueSoFar = node.val;
45 }
46
47 // Recursively traverse left subtree with updated max value
48 traverseAndCount(node.left, maxValueSoFar);
49
50 // Recursively traverse right subtree with updated max value
51 traverseAndCount(node.right, maxValueSoFar);
52 };
53
54 // Start DFS from root with initial max value as negative infinity
55 // Using -1000000 as the minimum possible value based on problem constraints
56 traverseAndCount(root, -1000000);
57
58 return goodNodeCount;
59}
60
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of nodes in the binary tree. The algorithm performs a depth-first search (DFS) traversal that visits each node exactly once. At each node, it performs constant time operations (comparison with mx
, incrementing ans
, and updating mx
), so the total time complexity is linear with respect to the number of nodes.
Space Complexity: O(h)
where h
is the height of the binary tree. The space complexity is determined by the recursion call stack. In the worst case of a skewed tree (essentially a linked list), the height would be n
, making the space complexity O(n)
. In the best case of a perfectly balanced tree, the height would be log(n)
, making the space complexity O(log n)
. The algorithm uses a constant amount of extra space for variables (ans
and mx
at each recursive call), but this doesn't change the overall space complexity which is dominated by the recursion stack depth.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Updating the Maximum Value Correctly
A frequent mistake is updating the max_so_far
variable globally rather than maintaining it separately for each path. This leads to incorrect results when the tree has multiple branches.
Incorrect Approach:
def goodNodes(self, root: TreeNode) -> int:
def dfs(node):
if node is None:
return
# Wrong: Using and updating a shared maximum for all paths
nonlocal global_max
if node.val >= global_max:
nonlocal good_node_count
good_node_count += 1
global_max = node.val # This affects all branches!
dfs(node.left)
dfs(node.right)
good_node_count = 0
global_max = float('-inf')
dfs(root)
return good_node_count
Why it's wrong: When we traverse down one branch and update global_max
, this incorrectly affects the evaluation of nodes in other branches. Each path from root should maintain its own maximum independently.
Correct Solution: Pass max_so_far
as a parameter to each recursive call:
def dfs(node, max_so_far):
if node is None:
return
if node.val >= max_so_far:
nonlocal good_node_count
good_node_count += 1
max_so_far = node.val # Local update for this path only
dfs(node.left, max_so_far) # Each call gets its own max
dfs(node.right, max_so_far)
2. Forgetting to Count the Root Node
Another pitfall is using an initial maximum value that's too large, causing the root node to not be counted as good.
Incorrect:
# Starting with 0 or a positive number might exclude valid root nodes dfs(root, 0) # Wrong if root.val is negative
Correct:
# Use negative infinity to ensure root is always counted
dfs(root, float('-inf'))
3. Modifying the Maximum Before the Comparison
Some implementations mistakenly update the maximum value before checking if the current node is good:
Incorrect:
def dfs(node, max_so_far):
if node is None:
return
# Wrong: Updating max before comparison
max_so_far = max(max_so_far, node.val)
if node.val >= max_so_far: # This will always be true!
nonlocal good_node_count
good_node_count += 1
Correct:
def dfs(node, max_so_far):
if node is None:
return
# Check first, then update
if node.val >= max_so_far:
nonlocal good_node_count
good_node_count += 1
max_so_far = node.val # Update after counting
4. Using Strict Greater Than Instead of Greater Than or Equal
The problem states a node is good if no ancestor has a value greater than the current node's value. This means nodes with values equal to the maximum should also be counted.
Incorrect:
if node.val > max_so_far: # Wrong: excludes nodes equal to max
Correct:
if node.val >= max_so_far: # Correct: includes nodes equal to max
Which of these pictures shows the visit order of a depth-first search?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!