← Back to DevBytes

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

Introduction to Inverting Binary Trees

The "Invert Binary Tree" problem is one of the most famous algorithmic challenges in computer science, partly due to a legendary tweet by Max Howell, the creator of Homebrew, who mentioned he was asked to solve it on a whiteboard at Google and couldn't. Despite its apparent simplicity, this problem tests fundamental understanding of tree data structures and traversal techniques.

What is a Binary Tree Inversion?

A binary tree is a hierarchical data structure where each node has at most two children, referred to as the left child and the right child. Inverting a binary tree means swapping the left and right children of every node in the tree. This operation creates a mirror image of the original tree.

For example, if you have a tree where the root has a left child with value 2 and a right child with value 3, after inversion, the root will have a left child with value 3 and a right child with value 2. This swapping happens recursively or iteratively for every node in the tree.

Why It Matters

Inverting a binary tree is more than just an interview question. It has practical applications in various domains:

From a learning perspective, this problem elegantly tests your understanding of recursion, tree traversal, and the ability to think about problems in terms of subproblems.

Understanding the Problem

The Problem Statement

Given the root of a binary tree, invert the tree, and return its root. The inversion should swap every left subtree with its corresponding right subtree at every level of the tree.

Here's a more formal definition: For every node in the tree, swap its left child with its right child. This operation should be performed on all nodes, from the root down to the leaf nodes.

Visualizing the Inversion

Let's visualize the inversion process with an example:

Original Tree:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

Inverted Tree:

     4
   /   \
  7     2
 / \   / \
9   6 3   1

Notice how every node's left and right children have been swapped. The root 4 still has children 2 and 7, but they've swapped positions. Similarly, 2's children (1 and 3) have swapped, and 7's children (6 and 9) have swapped.

Approaches to Solve the Problem

There are two primary approaches to solve this problem: recursive and iterative. Both have their merits and understanding both will make you a more versatile programmer.

Recursive Approach

The recursive approach is the most intuitive and elegant solution. The idea is simple: for each node, swap its left and right children, then recursively invert the left and right subtrees.

The base case is when the node is null (we've reached a leaf node's child), in which case we simply return null. The recursive case involves swapping the children and then making recursive calls on both children.

Iterative Approach

The iterative approach uses a queue or stack to traverse the tree level by level (breadth-first) or depth-first, swapping children as we go. This approach is useful when you want to avoid the potential stack overflow that can occur with deep recursion.

Implementation in JavaScript

Setting Up the Tree Structure

First, let's define the TreeNode class that we'll use throughout this tutorial:

class TreeNode {
  constructor(val, left = null, right = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

// Helper function to create a sample tree
function createSampleTree() {
  // Creating the tree:
  //      4
  //    /   \
  //   2     7
  //  / \   / \
  // 1   3 6   9
  
  const root = new TreeNode(4);
  root.left = new TreeNode(2);
  root.right = new TreeNode(7);
  root.left.left = new TreeNode(1);
  root.left.right = new TreeNode(3);
  root.right.left = new TreeNode(6);
  root.right.right = new TreeNode(9);
  
  return root;
}

Recursive Solution

Here's the recursive implementation:

function invertTree(root) {
  // Base case: if the tree is empty
  if (root === null) {
    return null;
  }
  
  // Swap the left and right children
  const temp = root.left;
  root.left = root.right;
  root.right = temp;
  
  // Recursively invert the subtrees
  invertTree(root.left);
  invertTree(root.right);
  
  // Return the root of the inverted tree
  return root;
}

// Usage
const tree = createSampleTree();
const invertedTree = invertTree(tree);

Let's break down how this works:

  1. We first check if the root is null. If it is, we return null (base case).
  2. We swap the left and right children of the current node using a temporary variable.
  3. We recursively call invertTree on the left subtree (which is now the original right subtree).
  4. We recursively call invertTree on the right subtree (which is now the original left subtree).
  5. We return the root, which now points to the inverted tree.

The time complexity is O(n), where n is the number of nodes in the tree, because we visit each node exactly once. The space complexity is O(h), where h is the height of the tree, due to the recursion stack.

Iterative Solution Using BFS

Here's an iterative implementation using breadth-first search with a queue:

function invertTreeIterative(root) {
  if (root === null) {
    return null;
  }
  
  const queue = [root];
  
  while (queue.length > 0) {
    const current = queue.shift();
    
    // Swap the left and right children
    const temp = current.left;
    current.left = current.right;
    current.right = temp;
    
    // Add children to the queue for processing
    if (current.left !== null) {
      queue.push(current.left);
    }
    if (current.right !== null) {
      queue.push(current.right);
    }
  }
  
  return root;
}

// Usage
const tree2 = createSampleTree();
const invertedTree2 = invertTreeIterative(tree2);

Iterative Solution Using DFS with Stack

Alternatively, you can use depth-first search with a stack:

function invertTreeDFS(root) {
  if (root === null) {
    return null;
  }
  
  const stack = [root];
  
  while (stack.length > 0) {
    const current = stack.pop();
    
    // Swap the left and right children
    const temp = current.left;
    current.left = current.right;
    current.right = temp;
    
    // Push children to the stack for processing
    if (current.left !== null) {
      stack.push(current.left);
    }
    if (current.right !== null) {
      stack.push(current.right);
    }
  }
  
  return root;
}

// Usage
const tree3 = createSampleTree();
const invertedTree3 = invertTreeDFS(tree3);

Testing and Validation

To verify our solutions work correctly, let's create a helper function to print the tree and test our implementations:

// Helper function to print tree level by level (BFS)
function printTree(root) {
  if (root === null) {
    console.log("Empty tree");
    return;
  }
  
  const queue = [root];
  const result = [];
  
  while (queue.length > 0) {
    const levelSize = queue.length;
    const currentLevel = [];
    
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      currentLevel.push(node.val);
      
      if (node.left !== null) queue.push(node.left);
      if (node.right !== null) queue.push(node.right);
    }
    
    result.push(currentLevel);
  }
  
  console.log(result);
}

// Test the recursive solution
console.log("Original Tree:");
const testTree = createSampleTree();
printTree(testTree);

console.log("\nInverted Tree (Recursive):");
const inverted = invertTree(testTree);
printTree(inverted);

// Expected output:
// Original Tree:
// [[4], [2, 7], [1, 3, 6, 9]]
//
// Inverted Tree (Recursive):
// [[4], [7, 2], [9, 6, 3, 1]]

You can also write more formal tests using a testing framework like Jest:

// Using Jest for testing
const { invertTree, invertTreeIterative, TreeNode, createSampleTree } = require('./invertTree');

describe('Invert Binary Tree', () => {
  test('should invert a complete binary tree', () => {
    const tree = createSampleTree();
    const inverted = invertTree(tree);
    
    expect(inverted.val).toBe(4);
    expect(inverted.left.val).toBe(7);
    expect(inverted.right.val).toBe(2);
    expect(inverted.left.left.val).toBe(9);
    expect(inverted.left.right.val).toBe(6);
    expect(inverted.right.left.val).toBe(3);
    expect(inverted.right.right.val).toBe(1);
  });
  
  test('should handle empty tree', () => {
    expect(invertTree(null)).toBeNull();
  });
  
  test('should handle single node tree', () => {
    const singleNode = new TreeNode(1);
    const inverted = invertTree(singleNode);
    
    expect(inverted.val).toBe(1);
    expect(inverted.left).toBeNull();
    expect(inverted.right).toBeNull();
  });
  
  test('iterative solution should produce same result as recursive', () => {
    const tree1 = createSampleTree();
    const tree2 = createSampleTree();
    
    const invertedRecursive = invertTree(tree1);
    const invertedIterative = invertTreeIterative(tree2);
    
    // Both should produce the same structure
    expect(invertedRecursive.left.val).toBe(invertedIterative.left.val);
    expect(invertedRecursive.right.val).toBe(invertedIterative.right.val);
  });
});

Best Practices

Choose the Right Approach

Handle Edge Cases

Always consider and test edge cases in your implementation:

Code Readability and Maintainability

Write clean, self-documenting code:

// Good: Clear variable names and comments
function invertTree(root) {
  // Base case: empty subtree
  if (root === null) {
    return null;
  }
  
  // Swap children using destructuring (modern JavaScript)
  [root.left, root.right] = [root.right, root.left];
  
  // Recursively invert subtrees
  invertTree(root.left);
  invertTree(root.right);
  
  return root;
}

Performance Considerations

Modern JavaScript Features

Take advantage of modern JavaScript features for cleaner code:

// Using destructuring assignment for swapping
function invertTree(root) {
  if (!root) return null;
  
  [root.left, root.right] = [root.right, root.left];
  
  invertTree(root.left);
  invertTree(root.right);
  
  return root;
}

// Using optional chaining and nullish coalescing
function invertTreeModern(root) {
  if (root == null) return null;
  
  const temp = root.left;
  root.left = invertTreeModern(root.right);
  root.right = invertTreeModern(temp);
  
  return root;
}

Common Mistakes to Avoid

Forgetting the Base Case

One of the most common mistakes is forgetting to handle the null case, which leads to errors when trying to access properties of null:

// Wrong: No base case
function invertTreeWrong(root) {
  // This will throw an error when root is null
  const temp = root.left;
  root.left = root.right;
  root.right = temp;
  
  invertTreeWrong(root.left);
  invertTreeWrong(root.right);
  
  return root;
}

// Correct: Always include base case
function invertTreeCorrect(root) {
  if (root === null) return null;  // Base case
  
  [root.left, root.right] = [root.right, root.left];
  invertTreeCorrect(root.left);
  invertTreeCorrect(root.right);
  
  return root;
}

Incorrect Swapping Order

Be careful with the order of operations when swapping and recursing:

// Wrong: Swapping after recursion means we're inverting twice
function invertTreeWrongOrder(root) {
  if (root === null) return null;
  
  invertTreeWrongOrder(root.left);
  invertTreeWrongOrder(root.right);
  
  // This swap happens after children are already inverted
  [root.left, root.right] = [root.right, root.left];
  
  return root;
}

// Correct: Swap first, then recurse
function invertTreeRightOrder(root) {
  if (root === null) return null;
  
  [root.left, root.right] = [root.right, root.left];
  invertTreeRightOrder(root.left);
  invertTreeRightOrder(root.right);
  
  return root;
}

Conclusion

Inverting a binary tree is a fundamental problem that every developer should understand. While it may seem simple at first glance, it touches on important concepts like recursion, tree traversal, and algorithmic thinking. The recursive solution offers elegance and readability, while the iterative approach provides safety against stack overflow for deep trees. By understanding both approaches, handling edge cases properly, and following best practices, you'll be well-equipped to solve this problem and similar tree-based challenges in your coding journey. Remember that the key to mastering this problem—and algorithmic problems in general—is not just memorizing the solution, but understanding the underlying principles of how and why the solution works.

— Ad —

Google AdSense will appear here after approval

← Back to all articles