Start learning free

LESSON 431 OF 569 · BINARY TREES

Balanced Binary Tree in JavaScript: O(n) Bottom-Up Height Check

Practice O(n) Read the full explanation

Given the root of a binary tree, return true if it is height-balanced, and false otherwise.

A binary tree is height-balanced when, at every node, the heights of its left and right subtrees differ by at most 1.

The height of a tree is the number of nodes on its longest path from the top down to a leaf. An empty tree has a height of 0, and it is balanced.

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

 

Example 1:

    3
   / \
  9   20
     /  \
    15   7

Input: root = [3,9,20,null,null,15,7]
Output: true
Explanation: At the root, the left subtree (9) has a height of 1 and the right subtree (20, 15, 7) a height of 2: they differ by 1. At 20, both subtrees have a height of 1, and the leaves have two empty subtrees.

Example 2:

          1
         / \
        2   2
       / \
      3   3
     / \
    4   4

Input: root = [1,2,2,3,3,null,null,4,4]
Output: false
Explanation: At the root, the left subtree has a height of 3 and the right subtree a height of 1: they differ by 2.

Example 3:

          1
         / \
        2   2
       /     \
      3       3
     /         \
    4           4

Input: root = [1,2,2,3,null,null,3,4,null,null,4]
Output: false
Explanation: The root is fine: both of its subtrees have a height of 3. But the left 2 has a left subtree of height 2 (3 and 4) and an empty right subtree, of height 0: they differ by 2. The rule must hold at every node, not only at the root.

 

Constraints:

  • The number of nodes in the tree is in the range [0, 5000].
  • 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.

Measuring a subtree's height again for every node above it repeats work. Find each subtree's height once, from the bottom up, and check the rule at the same time.

STEP 1 OF 9
    Ctrl + Enter to check

    JAVASCRIPT · BINARY TREES

    Balanced Binary Tree in JavaScript, explained

    Problem overview

    The method isBalanced(root) receives the root of a binary tree and returns true when the tree is height-balanced: at every node, the heights of the left and right subtrees differ by at most 1. The height of a tree is the number of nodes on its longest path from the top down to a leaf. An empty tree has height 0 and counts as balanced. The lesson provides the node type: a TreeNode with val, left and right, where a missing child is null.

    Written in level order, [3, 9, 20, null, null, 15, 7] is balanced: at the root the subtrees have heights 1 and 2, and at 20 both have height 1. The tree [1, 2, 2, 3, 3, null, null, 4, 4] is not: at the root the left subtree has height 3 and the right one height 1. The tree [1, 2, 2, 3, null, null, 3, 4, null, null, 4] is not either, although the root's two subtrees are equally tall: the left 2 has a left subtree of height 2 and an empty right subtree, so the rule fails at that node. The rule has to hold at every node, not only at the root.

    The statement asks for O(n) time, visiting each node once.

    Approach: compute heights from the bottom up and check on the way

    The obvious solution writes a height function and, at every node, compares height(node.left) with height(node.right), recursing into both children. It is correct, but each call to height walks a whole subtree again, so the nodes near the top are measured many times. On a long, thin tree this approaches O(n²).

    The faster way does both jobs in one trip. A recursive height returns the height of the tree under a node, but it also checks the rule at that node while the heights are at hand. When a node breaks the rule, height returns the signal -1. A real height is never negative, so -1 can't be confused with one. Every caller passes the signal up, and isBalanced only asks whether the final answer is -1.

    For the example trees, the root's height results are:

    [3,9,20,null,null,15,7]  isBalanced: true   height() = 3
    [1,2,2,3,3,null,null,4,4]  isBalanced: false  height() = -1
    [1,2,2,3,null,null,3,4,null,null,4]  isBalanced: false  height() = -1
    []  isBalanced: true   height() = 0
    [1,2,null,3]  isBalanced: false  height() = -1
    

    In the third tree, the left child 2 is where the rule fails. It returns -1, and the root passes that along without looking at its right side.

    Step by step

    1. Make isBalanced a thin wrapper. It returns this.height(root) !== -1. The strict comparison !== is true when the two sides differ, and no type conversion is involved.
    2. Declare the helper height(node). It returns the number of levels under node if that tree is balanced, and -1 if it isn't.
    3. Base case. If node is null (!node), return 0. An empty tree is balanced with height 0, and this check also ends the recursion past the leaves.
    4. Get the left height with a recursive call on node.left, in a let named leftHeight.
    5. Pass the signal up. If leftHeight is -1, return -1 at once. The whole tree is unbalanced, so the right side doesn't need to be examined.
    6. Get the right height the same way into rightHeight. A missing child yields 0 thanks to the base case.
    7. Pass the signal up again. This check is not optional: without it, -1 would be treated as a height, and it differs from an empty side's 0 by only 1, so the rule could pass by mistake.
    8. Check this node. If Math.abs(leftHeight - rightHeight) > 1, return -1. Every node runs this on the way back up, so the rule is applied everywhere. Math.abs drops the sign, so it doesn't matter which side is taller.
    9. Return the height: 1 for this node plus the larger of the two subtree heights, with Math.max.

    The solution in JavaScript

    class Solution {
        height(node) {
            if (!node) {
                return 0;
            }
            let leftHeight = this.height(node.left);
            if (leftHeight === -1) {
                return -1;
            }
            let rightHeight = this.height(node.right);
            if (rightHeight === -1) {
                return -1;
            }
            if (Math.abs(leftHeight - rightHeight) > 1) {
                return -1;
            }
            return 1 + Math.max(leftHeight, rightHeight);
        }
        isBalanced(root) {
            return this.height(root) !== -1;
        }
    }
    

    JavaScript details worth knowing:

    • !node as a null check. The lesson's nodes use null for a missing child, and !null is true. The same test is also true for undefined and for 0, but a node is an object, which is always truthy, so here it only catches the missing-child case.
    • this.height inside a class. Methods of a class don't see each other as plain names. To call height from another method, write this.height(...). When the recursion calls itself it uses this.height too.
    • Method order doesn't matter. isBalanced comes after height in the file, but the order in a class body has no effect on which methods can call which.
    • Recursion depth. The tree can be up to 800 levels deep. Recursion that deep is well within what Node's call stack allows.

    Complexity

    Time: O(n). Each node is visited once, and the work at a node is a constant number of comparisons. When a node returns -1, the calls above it return right away, so a failing tree is never visited more than once either.

    Space: O(h) for the call stack, where h is the height of the tree. It is about log n for a balanced tree and up to n for a chain. No other memory is used.

    Edge cases and common mistakes

    • Empty tree. [] is balanced. root is null, height returns 0, and 0 is not -1.
    • Single node. [1] has height 1 and is balanced. [1, 2] is balanced too: the heights are 1 and 0, which differ by 1.
    • A chain of two below the root. [1, 2, null, 3] is unbalanced: the root's left side has height 2 and its right side height 0.
    • Checking only the root. The third example tree has equal subtree heights at the root and is still unbalanced. The rule applies at every node.
    • Forgetting to check rightHeight for -1. The signal would then be used as a height, and an unbalanced right subtree could slip through as balanced.
    • Using a separate height function at each node. It gives the right answer and then repeats work, as explained above.

    Testing

    • [3, 9, 20, null, null, 15, 7] gives true.
    • [1, 2, 2, 3, 3, null, null, 4, 4] gives false.
    • [1, 2, 2, 3, null, null, 3, 4, null, null, 4] gives false.
    • [] gives true.
    • [1] gives true.
    • [1, 2] gives true.
    • [1, 2, 3] gives true.
    • [1, 2, null, 3] gives false.
    • A chain of 800 nodes, each the left child of the one above, gives false.
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free