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:
- Computer Graphics: Mirror operations on hierarchical scene graphs
- Game Development: Flipping game levels or character animations
- Data Processing: Transforming hierarchical data structures for different processing needs
- Algorithmic Thinking: It serves as a foundation for understanding more complex tree operations
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:
- We first check if the root is null. If it is, we return null (base case).
- We swap the left and right children of the current node using a temporary variable.
- We recursively call
invertTreeon the left subtree (which is now the original right subtree). - We recursively call
invertTreeon the right subtree (which is now the original left subtree). - 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
- Use recursive approach when the tree is balanced or relatively shallow, as it's more readable and concise.
- Use iterative approach when dealing with potentially deep trees to avoid stack overflow errors.
- Consider the tree structure - for very wide trees, DFS might be more memory-efficient than BFS.
Handle Edge Cases
Always consider and test edge cases in your implementation:
- Empty tree (null root)
- Single node tree
- Tree with only left children (skewed)
- Tree with only right children (skewed)
- Complete binary tree
- Very deep tree (for stack overflow testing)
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
- Both recursive and iterative solutions have O(n) time complexity.
- Recursive solution has O(h) space complexity due to call stack (h = height of tree).
- BFS iterative solution has O(w) space complexity where w is the maximum width of the tree.
- DFS iterative solution has O(h) space complexity, same as recursive.
- For balanced trees, h = log(n), so recursive is efficient. For skewed trees, h = n, which can cause stack overflow.
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.