← Back to DevBytes

Solving Balanced Binary Tree in JavaScript: Step-by-Step Guide

Introduction to Balanced Binary Trees

Balanced binary trees are a fundamental data structure concept that appears frequently in coding interviews and real-world applications. Understanding how to determine whether a binary tree is balanced is essential for any JavaScript developer looking to strengthen their algorithmic problem-solving skills.

What Is a Balanced Binary Tree?

A balanced binary tree is a binary tree in which the depth of the two subtrees of every node never differs by more than one. In other words, for each node in the tree, the height of its left subtree and the height of its right subtree must differ by at most one. This property must hold true recursively for every node, not just the root.

Formally, a binary tree is considered balanced if:

Why It Matters

Balanced binary trees matter because they guarantee O(log n) time complexity for search, insertion, and deletion operations. When a binary tree becomes unbalanced, it can degenerate into a structure resembling a linked list, where these operations degrade to O(n) time complexity. This performance difference becomes critical when dealing with large datasets.

From an interview perspective, the "Balanced Binary Tree" problem (LeetCode #110) tests your understanding of tree traversal, recursion, and the ability to optimize solutions by avoiding redundant computations. It is one of the most commonly asked tree problems in technical interviews at major tech companies.

Understanding the Problem

Problem Definition

Given a binary tree, determine if it is height-balanced. The tree is represented using TreeNode objects, where each node has a val, a left child, and a right child.

Here is the standard TreeNode definition used in most coding platforms:

function TreeNode(val, left, right) {
  this.val = (val === undefined ? 0 : val);
  this.left = (left === undefined ? null : left);
  this.right = (right === undefined ? null : right);
}

Examples to Clarify

Consider the following balanced tree:

      3
     / \
    9  20
      /  \
     15   7

This tree is balanced because:

Now consider this unbalanced tree:

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

This tree is unbalanced because at node 1, the left subtree has a height of 3 while the right subtree has a height of 1, giving a difference of 2, which exceeds the allowed maximum of 1.

Approaches to Solve the Problem

Naive Approach: Top-Down Recursion

The most intuitive approach is to check the balance condition at every node by computing the heights of its left and right subtrees. For each node, you calculate the height of both subtrees, check if their difference is at most 1, and then recursively verify that both subtrees are also balanced.

While this approach is straightforward, it has a significant drawback: it computes the height of the same subtrees multiple times. The time complexity becomes O(n²) in the worst case (when the tree is skewed), because for each of the n nodes, you may traverse up to O(n) nodes to compute the height.

Optimized Approach: Bottom-Up Recursion

The optimized approach uses a bottom-up strategy where you compute the height and check balance in a single pass. Instead of computing heights separately from the balance check, you return the height from each recursive call and simultaneously detect imbalances.

The key insight is that if any subtree is unbalanced, you can short-circuit and propagate that information upward without continuing unnecessary computations. This reduces the time complexity to O(n) since each node is visited exactly once.

Step-by-Step Implementation

Step 1: The Height Helper Function

First, let us create a helper function that computes the height of a tree. This function will be used in the naive approach:

function getHeight(node) {
  if (node === null) {
    return 0;
  }
  const leftHeight = getHeight(node.left);
  const rightHeight = getHeight(node.right);
  return Math.max(leftHeight, rightHeight) + 1;
}

This function recursively computes the height by taking the maximum of the left and right subtree heights and adding 1 for the current node. A null node has a height of 0.

Step 2: Naive Solution Implementation

Using the height helper, we can implement the naive top-down solution:

function isBalanced(root) {
  if (root === null) {
    return true;
  }

  const leftHeight = getHeight(root.left);
  const rightHeight = getHeight(root.right);

  const heightDiff = Math.abs(leftHeight - rightHeight);

  if (heightDiff > 1) {
    return false;
  }

  return isBalanced(root.left) && isBalanced(root.right);
}

This solution works correctly but is inefficient. For each node, it calls getHeight on both subtrees, and then recursively calls isBalanced on both subtrees, which again calls getHeight. This leads to redundant height calculations.

Step 3: Optimized Solution Implementation

Now let us implement the optimized bottom-up approach. The idea is to return a special value (such as -1) when a subtree is found to be unbalanced, allowing us to short-circuit immediately:

function isBalanced(root) {
  function checkHeight(node) {
    if (node === null) {
      return 0;
    }

    const leftHeight = checkHeight(node.left);
    if (leftHeight === -1) {
      return -1;
    }

    const rightHeight = checkHeight(node.right);
    if (rightHeight === -1) {
      return -1;
    }

    if (Math.abs(leftHeight - rightHeight) > 1) {
      return -1;
    }

    return Math.max(leftHeight, rightHeight) + 1;
  }

  return checkHeight(root) !== -1;
}

Let us break down how this works:

Step 4: Alternative Using a Result Object

Some developers prefer a more explicit approach using an object to carry both the height and balance status. This avoids the magic number -1 and can make the code more readable:

function isBalanced(root) {
  function check(node) {
    if (node === null) {
      return { balanced: true, height: 0 };
    }

    const left = check(node.left);
    if (!left.balanced) {
      return { balanced: false, height: 0 };
    }

    const right = check(node.right);
    if (!right.balanced) {
      return { balanced: false, height: 0 };
    }

    const heightDiff = Math.abs(left.height - right.height);
    const balanced = heightDiff <= 1;
    const height = Math.max(left.height, right.height) + 1;

    return { balanced, height };
  }

  return check(root).balanced;
}

This approach is functionally identical but communicates intent more clearly. The trade-off is slightly more memory usage due to object creation at each node.

Testing the Solution

Building Test Trees

To verify our solution, let us create test cases covering balanced trees, unbalanced trees, edge cases, and more:

// Helper to build a tree from an array (level-order)
function buildTree(arr) {
  if (arr.length === 0 || arr[0] === null) {
    return null;
  }
  const root = new TreeNode(arr[0]);
  const queue = [root];
  let i = 1;
  while (queue.length > 0 && i < arr.length) {
    const node = queue.shift();
    if (i < arr.length && arr[i] !== null) {
      node.left = new TreeNode(arr[i]);
      queue.push(node.left);
    }
    i++;
    if (i < arr.length && arr[i] !== null) {
      node.right = new TreeNode(arr[i]);
      queue.push(node.right);
    }
    i++;
  }
  return root;
}

// Test Case 1: Balanced tree
const tree1 = buildTree([3, 9, 20, null, null, 15, 7]);
console.log(isBalanced(tree1)); // Expected: true

// Test Case 2: Unbalanced tree
const tree2 = buildTree([1, 2, 2, 3, 3, null, null, 4, 4]);
console.log(isBalanced(tree2)); // Expected: false

// Test Case 3: Empty tree
const tree3 = buildTree([]);
console.log(isBalanced(tree3)); // Expected: true

// Test Case 4: Single node
const tree4 = buildTree([1]);
console.log(isBalanced(tree4)); // Expected: true

// Test Case 5: Left-skewed tree (unbalanced)
const tree5 = buildTree([1, 2, null, 3, null, 4, null]);
console.log(isBalanced(tree5)); // Expected: false

// Test Case 6: Right-skewed tree (unbalanced)
const tree6 = buildTree([1, null, 2, null, 3, null, 4]);
console.log(isBalanced(tree6)); // Expected: false

Running these test cases should confirm that the solution handles all scenarios correctly. The empty tree and single-node tree are important edge cases that should not be overlooked.

Complexity Analysis

Time Complexity

The optimized solution has a time complexity of O(n), where n is the number of nodes in the tree. This is because each node is visited exactly once during the bottom-up traversal. The short-circuit mechanism ensures that once an imbalance is detected, no further unnecessary computations are performed.

Space Complexity

The space complexity is O(h), where h is the height of the tree. This accounts for the recursion stack. In the best case (a balanced tree), h = O(log n). In the worst case (a skewed tree), h = O(n). For the naive approach, the space complexity remains O(h) for the recursion stack, but the time complexity degrades to O(n²).

Best Practices

Choose the Right Approach

Always prefer the bottom-up optimized approach over the naive top-down approach. The optimized version is not only more efficient but also demonstrates a deeper understanding of tree traversal patterns. In an interview setting, mentioning both approaches and explaining the trade-offs shows analytical thinking.

Handle Edge Cases Explicitly

Make sure your solution handles edge cases such as an empty tree (null root), a single-node tree, and completely skewed trees. These cases are common in test suites and can reveal subtle bugs in your implementation.

Use Clear Variable Names

Avoid cryptic variable names. Use descriptive names like leftHeight, rightHeight, and heightDiff instead of single letters. This makes your code easier to read and maintain, especially when revisiting it after some time.

Consider Iterative Solutions

While recursive solutions are elegant, extremely deep trees can cause stack overflow errors in JavaScript. For production environments dealing with very large trees, consider an iterative post-order traversal using an explicit stack. Here is a sketch of how that might look:

function isBalanced(root) {
  if (root === null) return true;

  const stack = [];
  const heights = new Map();
  let node = root;
  let lastVisited = null;

  while (stack.length > 0 || node !== null) {
    if (node !== null) {
      stack.push(node);
      node = node.left;
    } else {
      const peekNode = stack[stack.length - 1];
      if (peekNode.right !== null && lastVisited !== peekNode.right) {
        node = peekNode.right;
      } else {
        const current = stack.pop();
        const leftH = heights.get(current.left) || 0;
        const rightH = heights.get(current.right) || 0;

        if (Math.abs(leftH - rightH) > 1) {
          return false;
        }

        heights.set(current, Math.max(leftH, rightH) + 1);
        lastVisited = current;
      }
    }
  }

  return true;
}

This iterative approach uses a post-order traversal pattern and stores computed heights in a Map. It avoids recursion entirely, making it safe for very deep trees.

Test Thoroughly

Always test your solution with a variety of inputs. Beyond the basic test cases, consider trees where only one subtree is deep, trees where the imbalance occurs deep in the structure rather than at the root, and trees with negative values or duplicate values to ensure your logic is robust.

Common Pitfalls to Avoid

Conclusion

Solving the Balanced Binary Tree problem in JavaScript is an excellent exercise in understanding tree traversal, recursion, and algorithm optimization. The naive top-down approach provides a clear conceptual foundation, while the optimized bottom-up approach demonstrates how to eliminate redundant computations by combining height calculation with balance checking in a single pass. By mastering both approaches, understanding their time and space complexities, and being aware of common pitfalls, you will be well-equipped to tackle this problem and similar tree-based challenges in coding interviews and real-world development scenarios. Remember to always test your solution against edge cases, use clear and descriptive variable names, and consider iterative alternatives when dealing with potentially deep tree structures in production environments.

— Ad —

Google AdSense will appear here after approval

← Back to all articles