Problem overview
The function good_nodes receives the root of a binary tree, an Option<Box<TreeNode>>, and returns how many of its 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 type, with the fields val, left and right, and the tree always has at least one node.
In the tree [3, 1, 4, 3, None, 1, 5] the answer is 4. The good nodes are the root 3, the 4 (path 3, 4), the 5 (path 3, 4, 5) and the 3 at the bottom left (path 3, 1, 3, where nothing is greater than 3). The 1 under the root is not good, because 3 is greater, and neither is the 1 under the 4.
In [3, 3, None, 4, 2] the answer is 3. The root 3, the 3 under it (an equal value is not greater) and the 4 are good. The 2 is not, since its path holds two 3s.
The lesson asks for O(n) time, visiting each node once. Walking back up the path from every node would take up to O(n · h) for a tree h levels deep.
Approach: carry the largest value down the path
A node is good exactly when its value is at least the largest value above it. So there is no need to look at the whole path: carry one number down, the largest value seen so far on the path, and compare each node with it. This is a depth-first search with one extra parameter.
The recursive helper count_good(node, max_so_far) returns the number of good nodes in the tree under node, where max_so_far is the largest value on the path above it. At each node:
- if
node.val >= max_so_far, the node is good and counts 1; - the largest value on the path down to this node, itself included, is
max_so_far.max(node.val); - the answer is this node's count plus the good nodes in the left subtree plus those in the right, both measured against that new largest value.
Both children get the same new maximum. They share the path down to the current node, and nothing in the left branch lies on the right branch's path.
The starting value matters. The root is always good, so the search starts from the root's own value. Starting from 0 would be wrong when every value is negative: for the tree with root -5 and children -7 and -3, the correct answer is 2 (the root and the -3), but a search that starts from 0 counts no good nodes and returns 0.
Step by step
- Read the root's value into
root_val. The root is anOption, so writeroot.as_ref().unwrap().val:as_reflooks inside without taking the tree, andunwrapis safe because the tree has a node. - Call the helper with
rootandroot_valas the answer, without a semicolon. The value is read first because passingrootmoves it into the call. - Declare the helper
count_goodwith the parametersnode: Option<Box<TreeNode>>andmax_so_far: i32, returningi32. - Make the body a
matchonnode, with no semicolon after it, so its value is what the helper returns. - Base case: the
Nonearm gives 0. An empty tree has no good nodes, and this arm stops the recursion past a leaf. - The node arm:
Some(node)binds the box asnode, with a block body. - Start a counter:
let mut count = 0;. It will hold this node, if good, plus the good nodes below it. - Test the node: if
node.val >= max_so_far, setcountto 1. Equal counts as good. - Update the maximum: store
max_so_far.max(node.val)innew_max. - Add the left subtree:
count += Self::count_good(node.left, new_max); - Add the right subtree the same way with
node.right. - End the arm with
count, without a semicolon, so it is the arm's value.
The solution in Rust
impl Solution {
fn count_good(node: Option<Box<TreeNode>>, max_so_far: i32) -> i32 {
match node {
None => 0,
Some(node) => {
let mut count = 0;
if node.val >= max_so_far {
count = 1;
}
let new_max = max_so_far.max(node.val);
count += Self::count_good(node.left, new_max);
count += Self::count_good(node.right, new_max);
count
}
}
}
fn good_nodes(root: Option<Box<TreeNode>>) -> i32 {
let root_val = root.as_ref().unwrap().val;
Self::count_good(root, root_val)
}
}
The Rust details worth a closer look:
as_ref()thenunwrap(). Callingunwrap()directly onrootwould move the tree out of the variable.as_ref()turns theOption<Box<TreeNode>>into anOption<&Box<TreeNode>>, sorootstays whole.unwrap()would panic onNone, but the lesson's trees always have at least one node. Copyingval(ani32) out ends the borrow beforerootis moved on the next line.- Moving the subtrees.
Self::count_good(node.left, new_max)moves the left subtree into the call, and the same for the right one. Nothing needs them afterwards, so no clone is needed. - Shadowing in the match. The pattern
Some(node)reuses the namenode. Inside the arm it is theBox<TreeNode>, andnode.valreads through the box. let mut count = 0;needsmutbecause the next lines assign to and add to it. The type is inferred asi32from the function's return type..max().a.max(b)returns the larger of two numbers, sonew_maxis the larger of the old maximum and this node's value.- The last expression.
countwithout a semicolon is the arm's value, and thematchwithout a semicolon is the function's value.
Complexity
Time: O(n). Each node is visited once, and each visit does a comparison, a max and two calls.
Space: O(h) for the recursion, where h is the height of the tree. That is about log n for a balanced tree and up to n for a chain. The lesson's trees are at most 800 levels deep, which the recursion handles: a chain of 800 nodes with values decreasing downward returns 1.
Edge cases and common mistakes
- One node.
[1]returns 1: the root is always good. - Negative values. Start from the root's value, not from 0. With root -5 and children -7 and -3, the answer is 2.
- Equal values. Use
>=, not>. In the second example the 3 under the root is good because equal is not greater. - Increasing and decreasing chains. A chain 1, 2, 3 going down has three good nodes. A chain 9, 8, 7 has one, the root.
- Passing the node's value instead of the maximum. A child must be compared with the largest value on the whole path, not only its parent's value. In the first example the 1 under the 4 sits below 3 and 4, so it is not good.
- Forgetting to update the maximum. If
max_so_farwere passed down unchanged, every node would be compared only with the root, and a node smaller than a larger value in the middle of its path could wrongly count as good. - Reading the root after moving it. Once
rootis passed to the helper it can't be used again, which is whyroot_valis read first.
Testing
[3, 1, 4, 3, None, 1, 5]gives4.[3, 3, None, 4, 2]gives3.[1]gives1.- Root -5 with children -7 and -3 gives
2. - A chain 9, 8, 7 going down gives
1. - A chain 1, 2, 3 going down gives
3.
