Problem overview
The method GoodNodes receives the root of a binary tree (at least one node) and returns how many nodes are good. A node is good when no node on the path from the root down to it has a greater value. A node equal to the largest value above it is still good, and the root is always good, because nothing is above it. The lesson provides the TreeNode class, whose nodes have the fields val, left and right.
In the tree [3, 1, 4, 3, null, 1, 5] the answer is 4. The root 3 is good. The 1 under it is not (3 is greater). The 3 below that 1 is good, because nothing on its path is greater than 3. The 4 is good, the 1 under the 4 is not, and the 5 is good. In [3, 3, null, 4, 2] the answer is 3: the root, the 3 below it (equal is fine) and the 4 are good, and the 2 is not.
Approach: carry the largest value down the path
To decide whether a node is good, you need the largest value on the path above it. Walking back up the path from every node would take up to O(n * h) time for a tree h levels deep. Instead, carry that information down as you go: a recursive method receives the largest value seen so far on the path and passes a possibly larger one to its children.
At each node:
- If the node's value is at least
maxSoFar, it is good and counts for 1. - Compute
newMax, the larger ofmaxSoFarand the node's value. This is the largest value on the path down to this node, itself included. - Add the good nodes of the left subtree and the right subtree, both counted against
newMax. The two children share the path down to this node, and nothing from the left branch is on the right branch's path.
The root starts with its own value as maxSoFar, so it passes its own test. For the first example, the method visits the nodes in this order:
node 3, maxSoFar 3: good
node 1, maxSoFar 3: not good
node 3, maxSoFar 3: good
node 4, maxSoFar 3: good
node 1, maxSoFar 4: not good
node 5, maxSoFar 4: good
Four lines say "good", matching the answer 4.
Step by step
- In
GoodNodes, call the helperCountGoodonroot, withroot.valas the largest value so far, and return its answer. Starting from 0 would be wrong: when every value is negative, the root would fail its own test. - Declare
CountGoodas a public method returning anint, with aTreeNodeparameternodeand anintparametermaxSoFar. It returns the number of good nodes in the tree undernode. - Base case: if
node == null, return 0. An empty tree has no good nodes, and this stops the recursion past the leaves. - Declare
countas anintstarting at 0. - Test this node: if
node.val >= maxSoFar, setcountto 1. One comparison checks the whole path, becausemaxSoFaris its largest value. Equal counts as good. - Compute
newMaxwithMath.Max(maxSoFar, node.val). - Add the left subtree:
count += CountGood(node.left, newMax). - Add the right subtree the same way, with
node.right. - Return
count.
The solution in C#
class Solution {
public int CountGood(TreeNode node, int maxSoFar) {
if (node == null) {
return 0;
}
int count = 0;
if (node.val >= maxSoFar) {
count = 1;
}
int newMax = Math.Max(maxSoFar, node.val);
count += CountGood(node.left, newMax);
count += CountGood(node.right, newMax);
return count;
}
public int GoodNodes(TreeNode root) {
return CountGood(root, root.val);
}
}
The C# details worth knowing:
- Values are passed by value.
maxSoFaris anint, so each call gets its own copy. When the left call returns, the right call still starts from the samenewMax. No "undo" step is needed, as it would be with a shared list or a field. node == nullis the check for a missing child. Readingnode.valon anullreference would throw aNullReferenceException, so the base case comes first.count += ...adds the right-hand side to the variable and stores it back. It is the same ascount = count + ....Math.Maxis in theSystemnamespace. In a file of your own, addusing System;.- Helper before or after. C# lets a method call another written below or above it in the same class. The lesson writes
CountGoodfirst just to keep the order of the steps natural. - Recursion depth. Each call uses stack space. The tests have at most 800 levels. A chain of 800 nodes, the deepest the tests allow, runs without problems; a much deeper tree could cause a stack overflow, which cannot be caught in C#.
Complexity
Time: O(n). Each node is visited once, and the work per node is a comparison, a Math.Max and two calls.
Space: O(h), where h is the height of the tree, for the recursion. That is about log n for a balanced tree and n for a chain. Nothing else is stored.
Edge cases and common mistakes
- One node.
[1]gives 1: the root is always good. - Negative values. For
[-1, -5, -2]the answer is 1: only the root. If the helper started from 0 instead ofroot.val, the same tree would give 0, and the root would not be counted. - Equal values. A tree with three 2s (
[2, 2, 2]) gives 3. Using>instead of>=would leave out the equal ones. - Decreasing paths.
[5, 4, 3, 2]gives 1, only the root. In[-5, -1, -9]the answer is 2: the root and the -1. - Increasing paths.
[1, 2, 3, 4]gives 4, since every value is at least the maximum above it. - Updating
maxSoFarbefore testing the node. IfnewMaxis computed first and used in the test, every node would be compared with a value that already includes itself, and it would look good even when a larger value sits above it. - Comparing with the parent only. A node can be larger than its parent and still smaller than a grandparent. The whole path counts, which is why a running maximum is needed, and why the children must receive
newMax, not justnode.val.
Testing
[3, 1, 4, 3, null, 1, 5]gives4.[3, 3, null, 4, 2]gives3.[1]gives1.[-1, -5, -2]gives1.[-5, -1, -9]gives2.[5, 4, 3, 2]gives1.[1, 2, 3, 4]gives4.
