Start learning free

LESSON 430 OF 573 · BINARY TREES

Count Good Nodes in a Binary Tree in TypeScript: O(n) DFS with maxSoFar

Practice O(n) Read the full explanation

Given the root of a binary tree, return the number of good nodes in the tree.

A node X is good when no node on the path from the root down to X has a value greater than the value of X.

A node whose value equals the largest value above it is still good, and the root is always good: nothing is above it.

The trees below are written in level order (top to bottom, left to right), with null for a missing child.

 

Example 1:

      3
     / \
    1   4
   /   / \
  3   1   5

Input: root = [3,1,4,3,null,1,5]
Output: 4
Explanation: The good nodes are the root 3, then 4 (path 3 → 4), 5 (path 3 → 4 → 5) and the 3 at the bottom left (path 3 → 1 → 3: nothing on it is greater than 3). The 1 under the root is not good: 3 is greater. Neither is the 1 under 4: 3 and 4 are greater.

Example 2:

      3
     /
    3
   / \
  4   2

Input: root = [3,3,null,4,2]
Output: 3
Explanation: The root 3, the 3 under it (an equal value is not greater) and the 4 are good. The 2 is not: the path 3 → 3 → 2 has greater values.

Example 3:

Input: root = [1]
Output: 1
Explanation: The root is always good.

 

Constraints:

  • The number of nodes in the tree is in the range [1, 105].
  • Each node value is between -104 and 104.
  • The tree is at most 800 levels deep.

Note:

Your algorithm should run in O(n) time, where n is the number of nodes, visiting each node once.

Walking back up the path from every node would take up to O(n · h) time for a tree h levels deep. Instead, carry what you need down the path as you go.

STEP 1 OF 9
    Ctrl + Enter to check

    TYPESCRIPT · BINARY TREES

    Count Good Nodes In Binary Tree in TypeScript, explained

    Problem overview

    The method goodNodes receives the root of a binary tree, a TreeNode | null (the lesson provides the TreeNode class, with the fields val, left and right), and returns the number of good nodes. A node is good when no node on the path from the root down to it has a value greater than its own. A node that equals the largest value above it is still good, and the root is always good, because nothing is above it. The tree has at least one node.

    For the tree [3,1,4,3,null,1,5] (level order, null for a missing child), the answer is 4. The root 3 is good. The 4 is good (path 3, 4), and so is the 5 (path 3, 4, 5). The 3 at the bottom left is good too: its path 3, 1, 3 has nothing greater than 3. The 1 under the root is not good, because 3 is greater, and neither is the 1 under the 4.

    For [3,3,null,4,2] the answer is 3. The root, the 3 under it (an equal value is not greater) and the 4 are good. The 2 is not, since its path 3, 3, 2 holds greater values.

    Approach: carry the largest value down the path

    One way to decide for each node is to walk back up to the root and compare. That takes up to O(n · h) time for a tree h levels deep. There is a much simpler way, based on one fact: a node is good exactly when its value is at least the largest value above it. One comparison covers the whole path.

    So go down the tree with a depth-first search and carry that largest value with you, in a parameter named maxSoFar. At each node:

    • If node.val >= maxSoFar, the node is good and counts as 1.
    • The largest value on the path to the node's children is the larger of maxSoFar and node.val. Pass that to both children.
    • The answer for the node is its own count plus the answers for the left and right subtrees.

    For the root, there is nothing above it, so the starting value is the root's own value: root.val >= root.val is true, and the root is counted. Starting from 0 would be wrong when all values are negative.

    Here is the walk on the first example, printing each node's value and the maxSoFar it receives:

    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
    

    The left branch (1, then 3) is visited before the right branch (4, then 1 and 5). Four lines say "good", and the method returns 4. Both children of a node receive the same maxSoFar, because they share the path down to that node and nothing in the left branch is on the right branch's path.

    Step by step

    1. Start the search from goodNodes. Call a helper, countGood, on root with root.val as the largest value so far, and return its answer.
    2. Declare the helper above it, with the parameters node (TreeNode | null) and maxSoFar (number), returning a number.
    3. Base case: if node is null, return 0. An empty tree has no good nodes, and this stops the recursion past a leaf.
    4. Declare a counter count with let, starting at 0. It will hold this node (if it is good) plus the good nodes below.
    5. Test the node: if node.val >= maxSoFar, set count to 1. Equal counts as good.
    6. Compute newMax with Math.max(maxSoFar, node.val): the largest value on the path down to this node, the node included.
    7. Add the left subtree's count with += and a recursive call on node.left with newMax.
    8. Do the same for the right subtree, node.right.
    9. Return count.

    The solution in TypeScript

    class Solution {
        countGood(node: TreeNode | null, maxSoFar: number): number {
            if (!node) {
                return 0;
            }
            let count = 0;
            if (node.val >= maxSoFar) {
                count = 1;
            }
            let newMax = Math.max(maxSoFar, node.val);
            count += this.countGood(node.left, newMax);
            count += this.countGood(node.right, newMax);
            return count;
        }
    
        goodNodes(root: TreeNode | null): number {
            return this.countGood(root, root.val);
        }
    }
    

    Details that matter in TypeScript:

    • Calling the helper. Inside a class, a method calls another through this: this.countGood(...). A bare countGood(...) would not find it.
    • Union type and the null check. The parameter type TreeNode | null is a compile-time note, erased when the code is compiled to JavaScript. The base case if (!node) is what really protects the later lines node.val, node.left and node.right at run time.
    • root.val in goodNodes. The statement promises at least one node, so root is never null here. The lessons compile with --strict false, which is why the compiler accepts reading root.val on a TreeNode | null. If the root could be null, this line would throw a TypeError at run time.
    • let versus const. count changes, so it needs let. newMax never changes after it is set, so const would also work.
    • Math.max returns the larger of its arguments, here two numbers.

    Complexity

    Time: O(n). Each node is visited once, and each visit does a comparison, a Math.max and two recursive calls.

    Space: O(h), where h is the depth of the tree. The only extra memory is the call stack, one frame per level. It is 800 frames at most for the lesson's trees, which Node handles easily; a chain of 800 nodes, each larger than the last, returns 800.

    Edge cases and common mistakes

    • A single node. [1] gives 1: the root is always good.
    • All values negative. [-1,-2,-3] gives 1. Only the root is good: -2 and -3 are each smaller than -1. Starting maxSoFar at 0 instead of root.val would make the root fail the test and return 0.
    • Equal values. [-5,-5,-6,-4] gives 3: the root, the second -5 (equal, not smaller) and the -4. Using > instead of >= would reject the equal -5, and the root itself too.
    • A root with a smaller and a larger child. [2,1,3] gives 2: the root and the 3. The 1 has a greater value above it.
    • Forgetting newMax. Passing maxSoFar unchanged to the children would compare every node only with the root and call a node good when a larger value sits between it and the root.
    • Sharing a global maximum. A single variable that is updated as you go lets the left branch's values affect the right branch. The maximum must belong to the path, which is why it travels as a parameter.

    Testing

    With a small function that builds a tree from a level-order array, the method returned:

    • [3,1,4,3,null,1,5] gives 4.
    • [3,3,null,4,2] gives 3.
    • [1] gives 1.
    • [-1,-2,-3] gives 1.
    • [-5,-5,-6,-4] gives 3.
    • [2,1,3] gives 2.
    Keep going with TypeScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free