Module 5.1

Tree Basics & Traversals

Trees grow upside down in computer science - the root is at the top! Master these fascinating hierarchical structures that power everything from file systems to databases. Learn all four traversal methods (inorder, preorder, postorder, and level-order), the key to cracking 30%+ of coding interviews!

50 min read
Intermediate
High Frequency
What You'll Learn
  • Binary tree structure
  • Tree terminology
  • DFS traversals
  • BFS (level-order)
  • Tree properties
Contents
01

Introduction to Trees

Trees are everywhere in computer science! From your computer's file system to the DOM in web browsers, from database indexes to decision-making in AI. Understanding trees unlocks a whole new world of efficient algorithms and data organization.

Tree Data Structure

A Tree is a hierarchical data structure consisting of nodes connected by edges. Unlike linear structures (arrays, linked lists), trees branch out, with a single root node at the top and nodes below forming parent-child relationships.

Key properties: Trees are acyclic (no loops), connected (path exists between any two nodes), and have exactly n-1 edges for n nodes. A Binary Tree restricts each node to at most 2 children (left and right).

Binary Tree

Each node has at most 2 children - left and right. This is the most common tree type in interviews. Special variants include Binary Search Tree (BST), AVL Tree, Red-Black Tree, and Heap. Most tree problems you'll encounter use binary trees.

Use cases: Expression parsing, BST for searching, Heaps for priority queues, Huffman coding for compression.

N-ary Tree

Each node can have any number of children (0 to N). Children are typically stored in a list or array. File systems and DOM trees are real-world examples. Traversals work similarly but iterate over all children instead of just left/right.

Use cases: File system directories, HTML/XML DOM, organization hierarchies, game decision trees.

Why Trees? Represent hierarchical data (file systems, org charts), enable O(log n) search in BST, power databases (B-trees), and form the basis of heaps.

Practice Questions: Introduction to Trees

Task: Write a recursive function to count the total number of nodes in a binary tree.

Show Solution
int countNodes(TreeNode root) {
    if (root == null) return 0;
    return 1 + countNodes(root.left) + countNodes(root.right);
}

// Example: Tree with 5 nodes
//        1
//       / \
//      2   3
//     / \
//    4   5
// countNodes(root) returns 5

Task: Write a function that returns true if a given node is a leaf node (has no children).

Show Solution
boolean isLeaf(TreeNode node) {
    if (node == null) return false;
    return node.left == null && node.right == null;
}

// A leaf has NO children (both left and right are null)

Task: Write a recursive function to count only the leaf nodes in a binary tree.

Show Solution
int countLeaves(TreeNode root) {
    if (root == null) return 0;
    if (root.left == null && root.right == null) return 1;
    return countLeaves(root.left) + countLeaves(root.right);
}

// Example: Tree with 3 leaf nodes (4, 5, 3)
//        1
//       / \
//      2   3  <- leaf
//     / \
//    4   5    <- both leaves
// countLeaves(root) returns 3

Task: Write a recursive function to calculate the sum of all node values in the tree.

Show Solution
int sumOfNodes(TreeNode root) {
    if (root == null) return 0;
    return root.val + sumOfNodes(root.left) + sumOfNodes(root.right);
}

// Example:     5
//            /   \
//           3     8
//          / \   / \
//         1   4 7   9
// Sum = 5 + 3 + 8 + 1 + 4 + 7 + 9 = 37

Task: Find the maximum value node in a binary tree (not BST - values can be anywhere).

Show Solution
int findMax(TreeNode root) {
    if (root == null) return Integer.MIN_VALUE;
    
    int leftMax = findMax(root.left);
    int rightMax = findMax(root.right);
    
    return Math.max(root.val, Math.max(leftMax, rightMax));
}

// Alternative with helper to avoid Integer.MIN_VALUE edge case
Integer findMaxValue(TreeNode root) {
    if (root == null) return null;
    
    Integer leftMax = findMaxValue(root.left);
    Integer rightMax = findMaxValue(root.right);
    
    int maxVal = root.val;
    if (leftMax != null) maxVal = Math.max(maxVal, leftMax);
    if (rightMax != null) maxVal = Math.max(maxVal, rightMax);
    
    return maxVal;
}
02

Tree Terminology

Before diving into code, let's learn the vocabulary of trees. These terms will appear constantly in interviews and documentation, so understanding them is essential for clear communication.

Tree Terminology

Trees have their own language. The root is the topmost node (like a family patriarch), leaves are nodes with no children (end points), and height measures how tall the tree is from root to the deepest leaf.

Key insight: Depth is measured from top-down (root to node), while Height is measured bottom-up (node to deepest leaf). Root has depth 0, leaves have height 0.

Term Definition
Root Top-most node with no parent
Leaf Node with no children (external node)
Internal Node Node with at least one child
Parent Node directly above another node
Child Node directly below another node
Siblings Nodes sharing the same parent
Depth Distance from root to node (root = 0)
Height Distance from node to deepest leaf
Level All nodes at same depth
Subtree Tree formed by a node and its descendants
Visual Guide: Tree Terminology
                    ┌───────────────────────────────────────────────────┐
                               TREE TERMINOLOGY DIAGRAM                
                    └───────────────────────────────────────────────────┘

          Depth 0 (Level 0)           [A] ← ROOT (no parent)
                                     /   \
          Depth 1 (Level 1)       [B]     [C] ← INTERNAL NODES (have children)
                                 /   \       \
          Depth 2 (Level 2)   [D]    [E]     [F] ← LEAF NODES (no children)
                              /
          Depth 3 (Level 3) [G] ← Deepest LEAF

                    ┌───────────────────────────────────────────────────┐
                      HEIGHT of tree = 3 (longest path from root)      
                      HEIGHT of node B = 2 (path to deepest leaf G)    
                      DEPTH of node E = 2 (distance from root)         
                      B & C are SIBLINGS (same parent A)               
                      A is PARENT of B, B is CHILD of A                
                      Subtree rooted at B: {B, D, E, G}                
                    └───────────────────────────────────────────────────┘
              

Practice Questions: Tree Terminology

Task: Given a target value, find the depth (distance from root) of that node. Return -1 if not found.

Show Solution
int findDepth(TreeNode root, int target, int depth) {
    if (root == null) return -1;
    if (root.val == target) return depth;
    
    int leftDepth = findDepth(root.left, target, depth + 1);
    if (leftDepth != -1) return leftDepth;
    
    return findDepth(root.right, target, depth + 1);
}

// Usage: findDepth(root, targetVal, 0)

Task: Return a list of all node values at depth K (root is depth 0).

Show Solution
List nodesAtDepthK(TreeNode root, int k) {
    List result = new ArrayList<>();
    findAtDepth(root, k, 0, result);
    return result;
}

void findAtDepth(TreeNode node, int k, int depth, List result) {
    if (node == null) return;
    if (depth == k) {
        result.add(node.val);
        return;
    }
    findAtDepth(node.left, k, depth + 1, result);
    findAtDepth(node.right, k, depth + 1, result);
}

Task: Given a target value, find the height of that node (distance to deepest leaf in its subtree).

Show Solution
int findNodeHeight(TreeNode root, int target) {
    if (root == null) return -1;
    
    if (root.val == target) {
        return getHeight(root);
    }
    
    int leftResult = findNodeHeight(root.left, target);
    if (leftResult != -1) return leftResult;
    
    return findNodeHeight(root.right, target);
}

int getHeight(TreeNode node) {
    if (node == null) return -1;
    return 1 + Math.max(getHeight(node.left), getHeight(node.right));
}

Task: Find the lowest common ancestor of two nodes p and q in a binary tree.

Show Solution
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    // Base case: null or found one of the nodes
    if (root == null || root == p || root == q) {
        return root;
    }
    
    // Search in left and right subtrees
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    
    // If both sides return non-null, root is LCA
    if (left != null && right != null) {
        return root;
    }
    
    // Otherwise return the non-null side
    return left != null ? left : right;
}

// Example:       3
//              /   \
//             5     1
//            / \   / \
//           6   2 0   8
// LCA(5, 1) = 3
// LCA(5, 6) = 5
// LCA(6, 2) = 5

Task: Print all ancestors (path from root) of a given node value.

Show Solution
boolean printAncestors(TreeNode root, int target) {
    if (root == null) return false;
    
    if (root.val == target) return true;
    
    // If target is in left or right subtree, print this node
    if (printAncestors(root.left, target) || 
        printAncestors(root.right, target)) {
        System.out.print(root.val + " ");
        return true;
    }
    
    return false;
}

// To get as list instead:
List getAncestors(TreeNode root, int target) {
    List ancestors = new ArrayList<>();
    findAncestors(root, target, ancestors);
    return ancestors;
}

boolean findAncestors(TreeNode node, int target, List list) {
    if (node == null) return false;
    if (node.val == target) return true;
    
    if (findAncestors(node.left, target, list) || 
        findAncestors(node.right, target, list)) {
        list.add(node.val);
        return true;
    }
    return false;
}
03

Binary Tree Node Structure

Every tree is built from nodes. In Java, we create a simple class to represent each node, storing its value and pointers to its children. This is the foundation for all tree operations.

TreeNode Class

A TreeNode is like a container with three compartments: one for the value (the data it holds), and two for pointers to its left and right children. If a child doesn't exist, we use null.

Why pointers? Unlike arrays where elements are adjacent in memory, tree nodes can be anywhere in memory. Pointers (references) tell us where to find the children, creating the hierarchical structure.

class TreeNode {
    int val;           // The data stored in this node
    TreeNode left;     // Reference to left child (or null)
    TreeNode right;    // Reference to right child (or null)
}
The TreeNode class has three fields that work together to create the tree structure. The int val stores the actual data (we use integers here, but it could be any type like String or Object). The TreeNode left and TreeNode right are references (pointers) that hold the memory addresses of child nodes - think of them as arrows pointing to where the children live in memory. Notice how the class references itself - this is called a self-referential structure, which is what allows us to build recursive tree structures. When a child doesn't exist, we set the reference to null.
TreeNode(int val) {
    this.val = val;
    this.left = null;
    this.right = null;
}
This constructor takes only the value as a parameter and creates a new node in isolation. The this.val = val stores the passed value, while both left and right are explicitly set to null, meaning this node starts as a leaf node with no children attached. Use this constructor when you want to create nodes first and then manually connect them later using statements like node.left = anotherNode. This gives you full control over when and how the tree structure is built, making it ideal for step-by-step tree construction.
TreeNode(int val, TreeNode left, TreeNode right) {
    this.val = val;
    this.left = left;
    this.right = right;
}
This constructor accepts three parameters: the value and two pre-built subtrees (or null if a child doesn't exist). This is an example of constructor overloading - Java allows multiple constructors with different parameter lists, giving you flexibility in how you create objects. The real power of this constructor is constructor chaining: you can nest constructor calls inside each other to build entire tree structures in a single expression. The children are created first (as arguments), then passed to the parent node being constructed. This approach is extremely useful for writing test cases and solving LeetCode problems.
// Building this tree:
//        1
//       / \
//      2   3
//     / \
//    4   5

TreeNode root = new TreeNode(1);      // Create root
root.left = new TreeNode(2);          // Attach left child
root.right = new TreeNode(3);         // Attach right child
root.left.left = new TreeNode(4);     // Attach grandchildren
root.left.right = new TreeNode(5);
This approach builds the tree one node at a time. First, we create the root node with new TreeNode(1) - at this point, it's completely alone with no children. Then we create new nodes and assign them to the parent's left or right fields. The expression root.left.left uses chained access: start at root → follow the left pointer to reach node 2 → then access that node's left field to attach node 4. This step-by-step method is excellent for beginners because you can visualize each connection, debug easily by inspecting intermediate states, and modify the tree structure at any point by reassigning pointers.
// Same tree built in one expression
TreeNode root2 = new TreeNode(1,
    new TreeNode(2,
        new TreeNode(4),
        new TreeNode(5)),
    new TreeNode(3)
);
This elegant approach builds the entire tree in one nested expression. Java evaluates innermost expressions first, so new TreeNode(4) and new TreeNode(5) are created before anything else. These become arguments to new TreeNode(2, ...), which creates node 2 with nodes 4 and 5 as its children. Finally, the root node 1 is created with node 2 as its left child and node 3 as its right child. Notice how the code structure visually mirrors the tree shape! This technique is perfect for LeetCode problems, unit tests, and any situation where you need to quickly set up a tree. However, it can become hard to read for very deep or complex trees.
Quick Summary:
  1. TreeNode class: Each node stores a value (val), and two pointers (left, right) to child nodes
  2. Constructor 1: Creates a node with just a value, children default to null
  3. Constructor 2: Creates a node with value AND pre-built left/right subtrees
  4. Manual building: Create root first, then attach children via .left and .right
  5. Constructor chaining: Build entire tree in one expression (useful for tests!)

Practice Questions: Node Structure

Task: Build this tree using TreeNode class:

     10
    /  \
   5    15
  / \     \
 3   7    20
Show Solution
TreeNode root = new TreeNode(10);
root.left = new TreeNode(5);
root.right = new TreeNode(15);
root.left.left = new TreeNode(3);
root.left.right = new TreeNode(7);
root.right.right = new TreeNode(20);

// Or using constructor chaining:
TreeNode root2 = new TreeNode(10,
    new TreeNode(5, new TreeNode(3), new TreeNode(7)),
    new TreeNode(15, null, new TreeNode(20))
);

Task: Modify TreeNode class to include a parent pointer. Write a method to populate parent pointers for an existing tree.

Show Solution
class TreeNodeWithParent {
    int val;
    TreeNodeWithParent left, right, parent;
    
    TreeNodeWithParent(int val) {
        this.val = val;
    }
}

void populateParents(TreeNodeWithParent node, TreeNodeWithParent parent) {
    if (node == null) return;
    node.parent = parent;
    populateParents(node.left, node);
    populateParents(node.right, node);
}

// Usage: populateParents(root, null);

Task: Given array [1, 2, 3, 4, 5, null, 7] (level-order with nulls), build the tree.

Show Solution
TreeNode buildTree(Integer[] arr) {
    if (arr == null || arr.length == 0 || arr[0] == null) return null;
    
    TreeNode root = new TreeNode(arr[0]);
    Queue queue = new LinkedList<>();
    queue.offer(root);
    int i = 1;
    
    while (!queue.isEmpty() && i < arr.length) {
        TreeNode node = queue.poll();
        
        if (i < arr.length && arr[i] != null) {
            node.left = new TreeNode(arr[i]);
            queue.offer(node.left);
        }
        i++;
        
        if (i < arr.length && arr[i] != null) {
            node.right = new TreeNode(arr[i]);
            queue.offer(node.right);
        }
        i++;
    }
    return root;
}
04

DFS Traversals

Depth-First Search (DFS) explores a tree by going as deep as possible before backtracking. There are three ways to visit nodes, differing only in when you process the current node.

DFS Traversals (Inorder, Preorder, Postorder)

Think of visiting a node as "doing work" (printing, adding to list, etc.). The three DFS methods differ in WHEN you do the work: Preorder (work first, then children), Inorder (left child, work, right child), Postorder (children first, then work).

Memory trick: The name tells you when to visit the ROOT. Pre = before children, In = in-between children, Post = after children. For BST, Inorder gives sorted output!

Inorder

Left → Root → Right

Gives sorted order for BST

Preorder

Root → Left → Right

Used to copy/serialize tree

Postorder

Left → Right → Root

Used to delete tree

Recursive Implementation

// Inorder: Left → Root → Right
void inorder(TreeNode root) {
    if (root == null) return;
    inorder(root.left);
    System.out.print(root.val + " ");
    inorder(root.right);
}
Inorder traversal visits nodes in Left → Root → Right order. First, we recursively visit the entire left subtree, then process the current node (print its value), and finally visit the entire right subtree. The magic of inorder traversal: when applied to a Binary Search Tree, it produces elements in sorted ascending order! This is because in a BST, all left descendants are smaller and all right descendants are larger.
// Preorder: Root → Left → Right
void preorder(TreeNode root) {
    if (root == null) return;
    System.out.print(root.val + " ");
    preorder(root.left);
    preorder(root.right);
}
Preorder traversal visits nodes in Root → Left → Right order. We process the current node first (before exploring children), then recursively visit left subtree, then right subtree. This is perfect for copying/cloning a tree because you create the parent node before its children. It's also used to serialize trees for storage or transmission - you can reconstruct the exact tree structure from preorder output.
// Postorder: Left → Right → Root
void postorder(TreeNode root) {
    if (root == null) return;
    postorder(root.left);
    postorder(root.right);
    System.out.print(root.val + " ");
}
Postorder traversal visits nodes in Left → Right → Root order. We first visit both children completely, then process the current node last. This is essential for safely deleting a tree - you must delete children before their parent to avoid memory leaks. It's also used for evaluating expression trees and calculating properties that depend on children (like tree height or subtree sums).
// Example tree:    1
//                 / \
//                2   3
//               / \
//              4   5
// Inorder:   4, 2, 5, 1, 3
// Preorder:  1, 2, 4, 5, 3
// Postorder: 4, 5, 2, 3, 1
This example shows all three traversals on the same tree. Inorder (4,2,5,1,3): visits leftmost node 4 first, works its way up through 2, visits 2's right child 5, then root 1, finally 3. Preorder (1,2,4,5,3): starts at root 1, then explores entire left subtree (2→4→5) before right subtree (3). Postorder (4,5,2,3,1): finishes all leaves first (4,5), then their parent (2), then right subtree (3), finally root (1).
Traversal Order Visualization
1 2 3 4 5
INORDER
Left → Root → Right
4 → 2 → 5 → 1 → 3
"Left first, then me, then right"
✓ BST = sorted output!
PREORDER
Root → Left → Right
1 → 2 → 4 → 5 → 3
"Me first, then explore children"
✓ Copy/clone trees
POSTORDER
Left → Right → Root
4 → 5 → 2 → 3 → 1
"Children first, then me"
✓ Delete trees safely

Iterative with Stack

// Iterative Inorder
List inorderIterative(TreeNode root) {
    List result = new ArrayList<>();
    Stack stack = new Stack<>();
    TreeNode curr = root;
    
    while (curr != null || !stack.isEmpty()) {
        // Go left as far as possible
        while (curr != null) {
            stack.push(curr);
            curr = curr.left;
        }
        // Process current node
        curr = stack.pop();
        result.add(curr.val);
        // Go right
        curr = curr.right;
    }
    
    return result;
}
The iterative inorder traversal manually simulates the call stack that recursion uses automatically. We use a pointer curr to track our position and a stack to remember nodes to process later. The inner while loop pushes all left children onto the stack (going as deep left as possible). When we hit null, we pop a node, process it (add to result), then move to its right child to repeat the process. This mimics the recursive call pattern: go left → process → go right.
// Iterative Preorder
List preorderIterative(TreeNode root) {
    List result = new ArrayList<>();
    if (root == null) return result;
    
    Stack stack = new Stack<>();
    stack.push(root);
    
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        result.add(node.val);
        
        // Push right first so left is processed first
        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left);
    }
    
    return result;
}
Iterative preorder is the most intuitive of the three. We push the root onto the stack, then loop: pop a node, process it immediately (Root first!), then push its children. The key trick: we push the right child before the left child. Since stacks are LIFO (Last In, First Out), the left child will be popped and processed first. This ensures we visit nodes in Root → Left → Right order. Think of the stack as a "to-do list" where we always grab the most recent item.
// Iterative Postorder (using reverse preorder trick)
List postorderIterative(TreeNode root) {
    LinkedList result = new LinkedList<>();
    if (root == null) return result;
    
    Stack stack = new Stack<>();
    stack.push(root);
    
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        result.addFirst(node.val);  // Add to front!
        
        if (node.left != null) stack.push(node.left);
        if (node.right != null) stack.push(node.right);
    }
    
    return result;
}
Iterative postorder is tricky, but this solution uses a clever trick. Notice it looks almost like preorder, but with two key differences: (1) we push left before right (so right processes first), and (2) we use addFirst() to insert at the front of the result. This effectively does a "reverse preorder" (Root → Right → Left), then reverses it to get (Left → Right → Root) which is postorder! Using LinkedList makes addFirst() efficient (O(1) instead of O(n) for ArrayList).
Quick Summary: Why Use Iterative?
  • Avoid stack overflow - Deep trees (100,000+ nodes) can crash recursive solutions
  • Interview requirement - Interviewers often ask "can you do it without recursion?"
  • Better control - Easier to pause/resume traversal or add extra logic
  • Memory-efficient - You control the stack size, no function call overhead

Practice Questions: DFS Traversals

Task: Given this tree, write the inorder, preorder, and postorder sequences:

     8
    / \
   4   12
  / \   \
 2   6   14
Show Solution
Inorder (L-Root-R):   2, 4, 6, 8, 12, 14  (sorted for BST!)
Preorder (Root-L-R):  8, 4, 2, 6, 12, 14
Postorder (L-R-Root): 2, 6, 4, 14, 12, 8

Task: Modify inorder traversal to return a List instead of printing.

Show Solution
List inorderList(TreeNode root) {
    List result = new ArrayList<>();
    inorderHelper(root, result);
    return result;
}

void inorderHelper(TreeNode node, List result) {
    if (node == null) return;
    inorderHelper(node.left, result);
    result.add(node.val);
    inorderHelper(node.right, result);
}

Task: Implement inorder traversal with O(1) space (no stack, no recursion).

Hint: Use threaded binary tree concept (temporary links to predecessors).

Show Solution
List morrisInorder(TreeNode root) {
    List result = new ArrayList<>();
    TreeNode curr = root;
    
    while (curr != null) {
        if (curr.left == null) {
            result.add(curr.val);
            curr = curr.right;
        } else {
            // Find inorder predecessor
            TreeNode pred = curr.left;
            while (pred.right != null && pred.right != curr) {
                pred = pred.right;
            }
            
            if (pred.right == null) {
                pred.right = curr;  // Create thread
                curr = curr.left;
            } else {
                pred.right = null;  // Remove thread
                result.add(curr.val);
                curr = curr.right;
            }
        }
    }
    return result;
}
// O(n) time, O(1) space!
05

Level-Order Traversal (BFS)

While DFS goes deep, Breadth-First Search (BFS) explores level by level. It visits all nodes at depth 0, then all at depth 1, and so on. This is perfect for problems involving levels or shortest paths.

Level-Order Traversal (BFS)

BFS uses a Queue (FIFO) to visit nodes level by level. Start with the root, then process all its children, then all grandchildren, and so on. The queue ensures we finish one level before starting the next.

Key pattern: Capture queue.size() BEFORE the inner loop to know exactly how many nodes are in the current level. This lets you group nodes by level in your output.

// Basic Level Order - returns list of lists
List> levelOrder(TreeNode root) {
    List> result = new ArrayList<>();
    if (root == null) return result;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        int levelSize = queue.size();
        List currentLevel = new ArrayList<>();
        
        for (int i = 0; i < levelSize; i++) {
            TreeNode node = queue.poll();
            currentLevel.add(node.val);
            
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        
        result.add(currentLevel);
    }
    
    return result;
}
This is the standard BFS template you'll use in 90% of level-order problems. We use a Queue (implemented with LinkedList) to process nodes in FIFO order. The critical insight is capturing queue.size() at the start of each iteration - this tells us exactly how many nodes are in the current level. We then loop exactly that many times, polling each node, adding its value to the current level's list, and offering its children to the queue for the next level. After the inner loop, we add the completed level to our result.
// Example:     3
//             / \
//            9  20
//              /  \
//             15   7
// Output: [[3], [9,20], [15,7]]
Walking through the example: Start with queue containing just [3]. Level 0: size=1, poll node 3, add its children 9 and 20 to queue. Level 1: size=2 (we have 9,20), poll both, add 20's children (15,7). Level 2: size=2 (we have 15,7), poll both, no children to add. Result: [[3], [9, 20], [15, 7]]. Each inner list represents one level of the tree, from top to bottom.
// Zigzag Level Order
List> zigzagLevelOrder(TreeNode root) {
    List> result = new ArrayList<>();
    if (root == null) return result;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    boolean leftToRight = true;
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        LinkedList level = new LinkedList<>();
        
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            
            if (leftToRight) {
                level.addLast(node.val);
            } else {
                level.addFirst(node.val);
            }
            
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        
        result.add(level);
        leftToRight = !leftToRight;
    }
    
    return result;
}
Zigzag traversal alternates direction each level: left-to-right, then right-to-left, and so on. The trick is using a LinkedList for each level (not ArrayList) so we can efficiently add to either end. When going left-to-right, we use addLast(). When going right-to-left, we use addFirst() to reverse the order without actually reversing. The boolean flag leftToRight toggles after each level. This is a common variation asked in interviews (LeetCode #103).
Level-Order (BFS) Visualization
Example Tree
L0 L1 L2 3 9 20 15 7
Queue Processing Steps
Step 1: Level 0
Queue: [3] → Poll 3, add children
Output: [[3]]
Step 2: Level 1
Queue: [9, 20] → Poll both, add 15, 7
Output: [[3], [9, 20]]
Step 3: Level 2
Queue: [15, 7] → Poll both, no children
Output: [[3], [9, 20], [15, 7]]
KEY INSIGHT
Capture queue.size() BEFORE the inner loop to know exactly how many nodes belong to the current level. This separates levels in your output!

Practice Questions: Level-Order (BFS)

Task: Return the values visible from the right side of the tree (rightmost node at each level).

Show Solution
List rightSideView(TreeNode root) {
    List result = new ArrayList<>();
    if (root == null) return result;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            if (i == size - 1) {  // Last node in level
                result.add(node.val);
            }
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
    }
    return result;
}

Task: Return the average value of nodes at each level.

Show Solution
List averageOfLevels(TreeNode root) {
    List result = new ArrayList<>();
    if (root == null) return result;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        double sum = 0;
        
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            sum += node.val;
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        
        result.add(sum / size);
    }
    return result;
}

Task: Return level order from bottom to top (deepest level first).

Show Solution
List> levelOrderBottom(TreeNode root) {
    LinkedList> result = new LinkedList<>();
    if (root == null) return result;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        List level = new ArrayList<>();
        
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            level.add(node.val);
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        
        result.addFirst(level);  // Add to front!
    }
    return result;
}
06

Tree Properties

Trees have many properties we need to compute: height, depth, balance, symmetry. These functions form the building blocks for more complex tree algorithms and appear frequently in interviews.

Tree Properties and Validation

Common tree properties include height/depth (how tall), balance (are subtrees similar height), symmetry (mirror image of itself), and completeness (all levels filled except possibly the last).

Recursive pattern: Most property checks follow the same pattern: base case (null node), check current node, recursively check children, combine results. Master this pattern and you can solve most tree property problems!

// Maximum Depth (Height)
int maxDepth(TreeNode root) {
    if (root == null) return 0;
    return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
This is arguably the most important tree function to master. The height of a tree is the longest path from root to any leaf. The logic is beautifully simple: an empty tree has height 0 (base case). For any other node, its height is 1 (counting itself) plus the maximum height of its two subtrees. The recursion naturally explores all paths and Math.max ensures we keep only the longest one. This O(n) solution visits every node exactly once.
// Minimum Depth
int minDepth(TreeNode root) {
    if (root == null) return 0;
    if (root.left == null) return 1 + minDepth(root.right);
    if (root.right == null) return 1 + minDepth(root.left);
    return 1 + Math.min(minDepth(root.left), minDepth(root.right));
}
Minimum depth is the shortest path from root to any leaf (not just any null). This is trickier than maxDepth because of a common pitfall: if a node has only one child, we can't count the null side as a path (that's not a leaf!). So we add special cases: if left is null, we must go right, and vice versa. Only when both children exist do we take the minimum. This subtle difference trips up many candidates in interviews.
// Count Total Nodes
int countNodes(TreeNode root) {
    if (root == null) return 0;
    return 1 + countNodes(root.left) + countNodes(root.right);
}
Counting nodes follows the simplest recursive pattern. An empty tree has 0 nodes. Any other tree has 1 node (the root) plus all nodes in the left subtree plus all nodes in the right subtree. This same pattern can be adapted for many tree problems: sum of all values, count of leaves, count of nodes at depth k, etc. Just change what you're accumulating and how you combine results.
// Check if Balanced (height differs by at most 1)
boolean isBalanced(TreeNode root) {
    return height(root) != -1;
}

int height(TreeNode root) {
    if (root == null) return 0;
    
    int leftHeight = height(root.left);
    if (leftHeight == -1) return -1;
    
    int rightHeight = height(root.right);
    if (rightHeight == -1) return -1;
    
    if (Math.abs(leftHeight - rightHeight) > 1) return -1;
    
    return 1 + Math.max(leftHeight, rightHeight);
}
A balanced tree has subtrees whose heights differ by at most 1 at every node. The naive approach (check height difference at each node separately) is O(n²). This optimized version is O(n) by using -1 as a "flag" to indicate imbalance. As soon as any subtree is unbalanced, we propagate -1 upward immediately, short-circuiting further computation. This "early termination" pattern is powerful for validation problems where finding one violation is enough.
// Check if Symmetric
boolean isSymmetric(TreeNode root) {
    if (root == null) return true;
    return isMirror(root.left, root.right);
}

boolean isMirror(TreeNode t1, TreeNode t2) {
    if (t1 == null && t2 == null) return true;
    if (t1 == null || t2 == null) return false;
    return t1.val == t2.val && 
           isMirror(t1.left, t2.right) && 
           isMirror(t1.right, t2.left);
}
A symmetric tree is a mirror image of itself around its center. The trick is comparing the left subtree with the right subtree in a "crossed" manner: left's left with right's right, and left's right with right's left. The helper function isMirror takes two nodes and checks if they're mirror images. Both null? Mirror. One null? Not mirror. Values different? Not mirror. Same values? Recursively check their children in crossed order.
// Check if Same Tree
boolean isSameTree(TreeNode p, TreeNode q) {
    if (p == null && q == null) return true;
    if (p == null || q == null) return false;
    return p.val == q.val && 
           isSameTree(p.left, q.left) && 
           isSameTree(p.right, q.right);
}
Two trees are the same if they have identical structure AND identical values at every position. The logic mirrors isMirror but without the "crossing": we compare left with left, right with right. This function is useful as a building block for "subtree" problems. For example, to check if tree B is a subtree of tree A, search for a node in A with the same value as B's root, then use isSameTree to verify the entire subtree matches.
Tree Type Property
Full Binary Tree Every node has 0 or 2 children
Complete Binary Tree All levels filled except last, filled left to right
Perfect Binary Tree All internal nodes have 2 children, all leaves same level
Balanced Binary Tree Height of subtrees differ by at most 1

Practice Questions: Tree Properties

Task: Invert (mirror) a binary tree by swapping left and right children at every node.

Show Solution
TreeNode invertTree(TreeNode root) {
    if (root == null) return null;
    
    // Swap children
    TreeNode temp = root.left;
    root.left = root.right;
    root.right = temp;
    
    // Recursively invert subtrees
    invertTree(root.left);
    invertTree(root.right);
    
    return root;
}

Task: Check if a tree is a Full Binary Tree (every node has 0 or 2 children).

Show Solution
boolean isFullBinaryTree(TreeNode root) {
    if (root == null) return true;
    
    // Leaf node (0 children) - OK
    if (root.left == null && root.right == null) return true;
    
    // Has exactly one child - NOT full
    if (root.left == null || root.right == null) return false;
    
    // Has two children - check subtrees
    return isFullBinaryTree(root.left) && isFullBinaryTree(root.right);
}

Task: Find the diameter (longest path between any two nodes). Path may or may not pass through root.

Show Solution
int diameter = 0;

int diameterOfBinaryTree(TreeNode root) {
    diameter = 0;
    getHeight(root);
    return diameter;
}

int getHeight(TreeNode node) {
    if (node == null) return 0;
    
    int leftHeight = getHeight(node.left);
    int rightHeight = getHeight(node.right);
    
    // Update diameter: path through this node
    diameter = Math.max(diameter, leftHeight + rightHeight);
    
    return 1 + Math.max(leftHeight, rightHeight);
}
// Key insight: diameter through a node = leftHeight + rightHeight
07

Code Templates and Patterns

Master these reusable code templates. They appear in 80% of tree problems on coding interviews. Copy, adapt, and conquer!

The Power of Templates

Instead of solving each tree problem from scratch, recognize the underlying pattern and apply the corresponding template. This dramatically speeds up your problem-solving during interviews.

Basic Recursive Traversal

// Template: Process all nodes in tree
void traverse(TreeNode root) {
    if (root == null) return;  // Base case
    
    // PREORDER: Process here (before children)
    // Example: print(root.val);
    
    traverse(root.left);   // Recurse left
    
    // INORDER: Process here (between children)
    // Example: add to sorted list for BST
    
    traverse(root.right);  // Recurse right
    
    // POSTORDER: Process here (after children)
    // Example: delete node safely
}
This is the universal traversal template that forms the foundation of almost every tree problem. The structure is always the same: base case (null check), then three "slots" where you can do work. Where you place your logic determines the traversal order: before recursion = preorder, between recursion = inorder, after recursion = postorder. Memorize this skeleton and you can solve 90% of tree traversal problems by just filling in the processing logic at the right spot.
// Example: Print all values (preorder)
void printAll(TreeNode root) {
    if (root == null) return;
    System.out.println(root.val);  // Process
    printAll(root.left);
    printAll(root.right);
}
A concrete example using the template. We process (print) the current node before recursing to children, making this a preorder traversal. The elegance of recursion: with just 4 lines of code, we visit every single node in the tree. The call stack handles all the bookkeeping of which nodes to visit next and when to backtrack. This same pattern works for any "do something to every node" problem - just replace the print statement with your logic.
When to Use Each Order:
// PREORDER: Clone/Copy a tree
TreeNode cloneTree(TreeNode root) {
    if (root == null) return null;
    TreeNode copy = new TreeNode(root.val);  // Create copy FIRST
    copy.left = cloneTree(root.left);        // Then clone children
    copy.right = cloneTree(root.right);
    return copy;
}
Preorder processes the parent before its children. Use it when you need to create or know the parent first: cloning trees (must create parent node before attaching children), serialization (save root, then subtrees), path tracking (record node as you go down), and building tree copies. Think: "I need to do something with this node before I can handle its children."
// INORDER: Get sorted elements from BST
void getSorted(TreeNode root, List result) {
    if (root == null) return;
    getSorted(root.left, result);      // All smaller values first
    result.add(root.val);              // Current value in middle
    getSorted(root.right, result);     // All larger values last
}
Inorder processes left subtree, then current node, then right subtree. For a BST, this magically gives you elements in sorted order! Why? In a BST, all left descendants are smaller (visited first), then comes the node, then all right descendants are larger (visited last). Also useful for converting BST to sorted array, finding kth smallest element, and validating BST property.
// POSTORDER: Calculate height (need children's heights first)
int height(TreeNode root) {
    if (root == null) return 0;
    int leftH = height(root.left);     // Get left height first
    int rightH = height(root.right);   // Get right height first
    return 1 + Math.max(leftH, rightH); // Now can compute this node's height
}
Postorder processes children before the parent. Use it when the parent's result depends on its children's results: computing height (need children heights first), calculating subtree sums, safely deleting nodes (must delete children before parent to avoid memory leaks), and evaluating expression trees (need operand values before applying operator). Think: "I can't process this node until I know what my children returned."

Return Value from Subtrees

// Template: Compute something from subtrees
int compute(TreeNode root) {
    if (root == null) return BASE_VALUE;  // What to return for empty tree?
    
    int leftResult = compute(root.left);   // Get result from left subtree
    int rightResult = compute(root.right); // Get result from right subtree
    
    // Combine: How to merge left, right, and current node?
    return COMBINE(leftResult, rightResult, root.val);
}
This is the "bottom-up" computation template - one of the most powerful patterns in tree problems. The idea: recursively get results from both subtrees, then combine them with the current node's value to produce the result for this subtree. You need to decide two things: (1) BASE_VALUE - what should an empty tree return? (2) COMBINE - how do you merge left result, right result, and current node? Once you answer these, the recursion handles everything else.
// Example: Count all nodes
int countNodes(TreeNode root) {
    if (root == null) return 0;
    int left = countNodes(root.left);
    int right = countNodes(root.right);
    return left + right + 1;  // Both subtrees + this node
}
The simplest application of the template. BASE_VALUE = 0 (empty tree has 0 nodes). COMBINE = left + right + 1 (total nodes = left subtree nodes + right subtree nodes + 1 for current node). This pattern extends naturally: count leaves? Return 1 only if both children are null. Count nodes at depth k? Pass depth as a parameter. The template is infinitely adaptable.
// Example: Sum all values
int sumNodes(TreeNode root) {
    if (root == null) return 0;
    return root.val + sumNodes(root.left) + sumNodes(root.right);
}
Another classic application. BASE_VALUE = 0 (empty tree contributes 0 to sum). COMBINE = root.val + left + right (total sum = this node's value + sum of left subtree + sum of right subtree). Notice how concise this is - we can inline the recursive calls directly in the return statement. This works whenever the combination operation is simple (addition, multiplication, etc.).
// Example: Maximum value
int maxValue(TreeNode root) {
    if (root == null) return Integer.MIN_VALUE;
    int leftMax = maxValue(root.left);
    int rightMax = maxValue(root.right);
    return Math.max(root.val, Math.max(leftMax, rightMax));
}
Finding the maximum requires a different base case: BASE_VALUE = Integer.MIN_VALUE. Why? Because any real node value should "win" against an empty subtree. If we used 0, a tree with all negative values would incorrectly return 0. The COMBINE uses nested Math.max to find the largest among left max, right max, and current value. Same pattern works for minimum (use Integer.MAX_VALUE and Math.min).
// Example: Height/Depth
int height(TreeNode root) {
    if (root == null) return 0;  // or -1 depending on definition
    return 1 + Math.max(height(root.left), height(root.right));
}
Height calculation is a special case where we only care about the longer path. BASE_VALUE = 0 (empty tree has height 0, or -1 if you define height as number of edges). COMBINE = 1 + max(left, right) (height = 1 for this node + the taller subtree). Note: we don't add both subtree heights - we only take the max because height is the longest path, not the total. This subtle difference is important for tree problems.

Two-Node Comparison

// Template: Compare two nodes/trees simultaneously
boolean compare(TreeNode node1, TreeNode node2) {
    // Both null: trees match at this point
    if (node1 == null && node2 == null) return true;
    
    // One null, other not: structure differs
    if (node1 == null || node2 == null) return false;
    
    // Both non-null: check current + recurse
    // Customize comparison logic here
    return node1.val == node2.val && 
           compare(node1.left, node2.left) &&
           compare(node1.right, node2.right);
}
This template handles the tricky null-checking logic that appears in all two-tree comparison problems. The pattern has three cases: (1) both null = match (we've reached the end of both branches together), (2) one null, one not = mismatch (structure differs), (3) both non-null = check values and recurse. The key insight is that cases 1 and 2 must be checked before accessing any node properties to avoid NullPointerException. Master this pattern and adapt the comparison logic for different problems.
// Example: Same Tree
boolean isSameTree(TreeNode p, TreeNode q) {
    if (p == null && q == null) return true;
    if (p == null || q == null) return false;
    return p.val == q.val && 
           isSameTree(p.left, q.left) && 
           isSameTree(p.right, q.right);
}
The most direct application of the template. Two trees are "same" if they have identical structure AND identical values at every corresponding position. We compare left-with-left and right-with-right (parallel comparison). The short-circuit evaluation of && means if values don't match, we don't even bother checking subtrees. This function becomes a building block for more complex problems like "is subtree" (LeetCode #572).
// Example: Symmetric Tree (mirror comparison)
boolean isSymmetric(TreeNode root) {
    return isMirror(root, root);
}

boolean isMirror(TreeNode t1, TreeNode t2) {
    if (t1 == null && t2 == null) return true;
    if (t1 == null || t2 == null) return false;
    return t1.val == t2.val && 
           isMirror(t1.left, t2.right) &&   // Cross-compare!
           isMirror(t1.right, t2.left);
}
Symmetric tree (LeetCode #101) uses a clever twist: instead of parallel comparison, we do crossed comparison. We compare t1's left with t2's right, and t1's right with t2's left. This checks if the tree is a mirror image of itself. The trick is calling isMirror(root, root) to compare the tree with itself in mirror mode. Notice how similar this is to isSameTree - only the recursive call order changes! Same template, different comparison logic.
// Example: Is Subtree
boolean isSubtree(TreeNode root, TreeNode subRoot) {
    if (root == null) return false;
    if (isSameTree(root, subRoot)) return true;
    return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
This problem (LeetCode #572) combines two patterns: tree traversal + tree comparison. The idea: traverse the main tree looking for a node that could be the root of the subtree. At each node, use isSameTree to check if the subtree starting here matches subRoot. If not, recursively check left and right children. The || means we return true as soon as we find a match anywhere. This is O(m×n) in worst case, where m and n are tree sizes. More efficient solutions exist using tree hashing.

Level-Order (BFS) Pattern

// Template: Process tree level by level
List> levelOrder(TreeNode root) {
    List> result = new ArrayList<>();
    if (root == null) return result;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        int levelSize = queue.size();  // CRUCIAL: snapshot before loop
        List currentLevel = new ArrayList<>();
        
        for (int i = 0; i < levelSize; i++) {
            TreeNode node = queue.poll();
            
            // PROCESS NODE HERE
            currentLevel.add(node.val);
            
            // Add children for next level
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        
        result.add(currentLevel);
    }
    return result;
}
This is the gold-standard BFS template for level-order traversal. The key insight is capturing queue.size() before the inner loop - this tells us exactly how many nodes belong to the current level. Without this snapshot, we'd mix nodes from different levels as we add children. The inner for loop processes exactly one level, then the outer while moves to the next. This template solves dozens of problems: level averages, largest value per level, connect nodes at same level, etc.
// Variation: Right Side View (last of each level)
List rightSideView(TreeNode root) {
    List result = new ArrayList<>();
    if (root == null) return result;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            if (i == size - 1) result.add(node.val);  // Last node
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
    }
    return result;
}
Right Side View (LeetCode #199) asks: "What nodes would you see looking at the tree from the right?" Answer: the last node at each level. The solution is almost identical to basic level-order - we just add a condition: if (i == size - 1) to only record the last node of each level. For Left Side View, change to if (i == 0). This pattern extends to many "per-level" problems: find max/min/sum at each level, count nodes at each level, etc.
// Variation: Zigzag (alternate direction)
List> zigzagLevelOrder(TreeNode root) {
    List> result = new ArrayList<>();
    if (root == null) return result;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    boolean leftToRight = true;
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        LinkedList level = new LinkedList<>();
        
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            if (leftToRight) level.addLast(node.val);
            else level.addFirst(node.val);
            
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        
        result.add(level);
        leftToRight = !leftToRight;  // Flip direction
    }
    return result;
}
Zigzag Level Order (LeetCode #103) alternates direction: level 0 left-to-right, level 1 right-to-left, and so on. The trick: use LinkedList instead of ArrayList for efficient insertion at either end. When going left-to-right, use addLast(). When going right-to-left, use addFirst() to reverse without actually reversing. The boolean leftToRight toggles after each level. Notice we still traverse the tree in normal order - we only change where we insert into the result list.

Path Problems

// Template: Find/Track paths in tree
// Pattern 1: Root-to-Leaf paths
List> allPaths = new ArrayList<>();

void findPaths(TreeNode root, List currentPath) {
    if (root == null) return;
    
    currentPath.add(root.val);
    
    if (root.left == null && root.right == null) {
        // Reached leaf: save path copy
        allPaths.add(new ArrayList<>(currentPath));
    } else {
        findPaths(root.left, currentPath);
        findPaths(root.right, currentPath);
    }
    
    currentPath.remove(currentPath.size() - 1);  // Backtrack!
}
This is the backtracking template for path problems. The pattern: (1) add current node to path, (2) if leaf, save a copy of the path (not the path itself - it will change!), (3) otherwise recurse on children, (4) backtrack by removing the current node. The backtrack step is crucial - it restores the path to its previous state so sibling branches start fresh. Without it, paths would accumulate incorrectly. This template works for: all root-to-leaf paths, paths with specific sum, paths as strings, etc.
// Example: Path Sum (does any root-to-leaf path sum to target?)
boolean hasPathSum(TreeNode root, int targetSum) {
    if (root == null) return false;
    
    // Leaf node: check if remaining sum equals node value
    if (root.left == null && root.right == null) {
        return root.val == targetSum;
    }
    
    // Recurse with reduced target
    return hasPathSum(root.left, targetSum - root.val) ||
           hasPathSum(root.right, targetSum - root.val);
}
Path Sum (LeetCode #112) asks if any root-to-leaf path sums to a target. Instead of tracking the path explicitly, we use a clever trick: subtract as we go. Start with the target, subtract each node's value as we descend. At a leaf, if the remaining target equals the leaf's value, we found a valid path! The || short-circuits: if left subtree finds a path, we don't check right. This O(n) solution is cleaner than maintaining a running sum because we don't need to pass extra state.
// Example: All paths with given sum
List> pathSum(TreeNode root, int targetSum) {
    List> result = new ArrayList<>();
    findPathsWithSum(root, targetSum, new ArrayList<>(), result);
    return result;
}

void findPathsWithSum(TreeNode node, int remaining, 
                      List path, List> result) {
    if (node == null) return;
    
    path.add(node.val);
    
    if (node.left == null && node.right == null && remaining == node.val) {
        result.add(new ArrayList<>(path));
    }
    
    findPathsWithSum(node.left, remaining - node.val, path, result);
    findPathsWithSum(node.right, remaining - node.val, path, result);
    
    path.remove(path.size() - 1);  // Backtrack
}
Path Sum II (LeetCode #113) extends the previous problem: find all paths that sum to target, not just check existence. This combines the backtracking template with the "subtract as you go" trick. Key differences from hasPathSum: (1) we track the actual path in a list, (2) we don't short-circuit - we explore both subtrees to find all valid paths, (3) we backtrack after exploring. Notice the condition at leaf: remaining == node.val (not 0) because we haven't subtracted the leaf's value yet.
// Pattern 2: Maximum Path Sum (any-to-any, harder)
int maxPathSum = Integer.MIN_VALUE;

int maxPathSum(TreeNode root) {
    maxPathSum = Integer.MIN_VALUE;
    maxGain(root);
    return maxPathSum;
}

int maxGain(TreeNode node) {
    if (node == null) return 0;
    
    // Max gain from left/right (ignore negative paths)
    int leftGain = Math.max(maxGain(node.left), 0);
    int rightGain = Math.max(maxGain(node.right), 0);
    
    // Path through this node as highest point
    int pathThroughNode = node.val + leftGain + rightGain;
    maxPathSum = Math.max(maxPathSum, pathThroughNode);
    
    // Return max gain if this node is part of path to ancestor
    return node.val + Math.max(leftGain, rightGain);
}
Binary Tree Maximum Path Sum (LeetCode #124, Hard) is a classic. The path can start and end at any node, not just root-to-leaf. The key insight: at each node, consider it as the "apex" of an arch-shaped path (left branch → node → right branch). We track maxPathSum globally. But when returning to the parent, we can only extend ONE branch (paths can't fork), so we return node.val + max(left, right). The Math.max(..., 0) ignores negative branches - it's better to not include them at all. This dual logic (global update vs return value) is what makes this problem tricky.

Tree Construction

// Template: Build tree from traversals
// Build from Preorder + Inorder
int preorderIndex = 0;
Map inorderMap = new HashMap<>();

TreeNode buildTree(int[] preorder, int[] inorder) {
    preorderIndex = 0;
    for (int i = 0; i < inorder.length; i++) {
        inorderMap.put(inorder[i], i);
    }
    return buildTreeHelper(preorder, 0, inorder.length - 1);
}
Construct Binary Tree from Preorder and Inorder (LeetCode #105) is a classic reconstruction problem. The key insight: preorder's first element is always the root, and inorder tells us which nodes are in left vs right subtree (everything left of root in inorder = left subtree). We use a HashMap for O(1) lookup of root's position in inorder. The preorderIndex advances globally as we build nodes because preorder visits nodes in the exact order we create them (root → left subtree → right subtree).
TreeNode buildTreeHelper(int[] preorder, int left, int right) {
    if (left > right) return null;
    
    int rootVal = preorder[preorderIndex++];
    TreeNode root = new TreeNode(rootVal);
    
    int inorderIndex = inorderMap.get(rootVal);
    
    root.left = buildTreeHelper(preorder, left, inorderIndex - 1);
    root.right = buildTreeHelper(preorder, inorderIndex + 1, right);
    
    return root;
}
The helper function does the actual building. It takes the valid range in inorder array (left to right). Base case: when left > right, there are no nodes in this subtree. Otherwise: (1) get next value from preorder as root, (2) find its position in inorder, (3) recursively build left subtree (elements before root in inorder), (4) build right subtree (elements after root). The order matters: we must build left before right because preorderIndex advances sequentially through preorder array.
// Build from level-order array (LeetCode format)
TreeNode buildFromArray(Integer[] arr) {
    if (arr == null || arr.length == 0 || arr[0] == null) return null;
    
    TreeNode root = new TreeNode(arr[0]);
    Queue queue = new LinkedList<>();
    queue.offer(root);
    int i = 1;
    
    while (!queue.isEmpty() && i < arr.length) {
        TreeNode node = queue.poll();
        
        if (i < arr.length && arr[i] != null) {
            node.left = new TreeNode(arr[i]);
            queue.offer(node.left);
        }
        i++;
        
        if (i < arr.length && arr[i] != null) {
            node.right = new TreeNode(arr[i]);
            queue.offer(node.right);
        }
        i++;
    }
    return root;
}
This builds a tree from LeetCode's level-order format: [1, 2, 3, null, 5]. The array represents the tree level by level, with null for missing nodes. We use BFS (queue) to build level by level, mirroring how we'd traverse. For each node we poll, the next two array elements are its left and right children. We use Integer[] (not int[]) to allow nulls. This is incredibly useful for testing - you can quickly create any tree structure from LeetCode's test cases without manually linking nodes.

Serialization

// Template: Convert tree to string and back
// Preorder with null markers
public String serialize(TreeNode root) {
    StringBuilder sb = new StringBuilder();
    serializeHelper(root, sb);
    return sb.toString();
}

void serializeHelper(TreeNode node, StringBuilder sb) {
    if (node == null) {
        sb.append("null,");
        return;
    }
    sb.append(node.val).append(",");
    serializeHelper(node.left, sb);
    serializeHelper(node.right, sb);
}
Serialize and Deserialize Binary Tree (LeetCode #297, Hard) converts a tree to a string for storage/transmission, then rebuilds it. The serialize function uses preorder traversal with null markers. Why preorder? Because the first value is the root, making deserialization straightforward. Why null markers? Without them, we can't distinguish between different tree structures with the same values. We use StringBuilder for efficient string concatenation (don't use += in loops - it's O(n²)!). The comma separator lets us split the string later.
public TreeNode deserialize(String data) {
    String[] nodes = data.split(",");
    Queue queue = new LinkedList<>(Arrays.asList(nodes));
    return deserializeHelper(queue);
}

TreeNode deserializeHelper(Queue queue) {
    String val = queue.poll();
    if (val.equals("null")) return null;
    
    TreeNode node = new TreeNode(Integer.parseInt(val));
    node.left = deserializeHelper(queue);
    node.right = deserializeHelper(queue);
    return node;
}
Deserialization rebuilds the tree from the string. We split by comma and use a Queue to process values in order. The recursive logic mirrors serialization: poll a value, if "null" return null, otherwise create a node and recursively build left then right subtrees. The queue automatically advances through the string as we poll. Key insight: since we serialized in preorder, we deserialize in preorder too - the order of recursive calls must match! This guarantees we reconstruct the exact same tree.
// Example serialized: "1,2,null,null,3,4,null,null,5,null,null"
// Represents:
//       1
//      / \
//     2   3
//        / \
//       4   5
Understanding the format: the string "1,2,null,null,3,4,null,null,5,null,null" encodes the tree in preorder. Reading left to right: 1 (root), 2 (left child of 1), null,null (2 has no children), 3 (right child of 1), 4 (left child of 3), null,null (4 has no children), 5 (right child of 3), null,null (5 has no children). Every node, including leaves, outputs exactly two null markers for its missing children. This explicit encoding makes reconstruction unambiguous.
08

Common Mistakes and How to Avoid Them

Even experienced programmers make these mistakes. Learn them now to avoid debugging headaches later!

Debug Smarter, Not Harder

Most tree bugs fall into predictable categories: null pointer exceptions, wrong base cases, or forgetting to return values. Recognizing these patterns helps you debug faster.

1
Forgetting Null Check
// WRONG: NullPointerException if root is null
int height(TreeNode root) {
    return 1 + Math.max(height(root.left), 
                        height(root.right));
}

This code crashes immediately when called with an empty tree (null root). Without checking for null first, accessing root.left or root.right throws a NullPointerException. Every recursive tree function must handle the base case where the node doesn't exist.

// CORRECT: Always check null first
int height(TreeNode root) {
    if (root == null) return 0;  // Base case!
    return 1 + Math.max(height(root.left), 
                        height(root.right));
}

The null check acts as the recursion's base case and termination condition. When root is null, return 0 (no height). This pattern should be your muscle memory - always start tree recursion with the null check before accessing any node properties.

2
Wrong Return in Recursion
// WRONG: Returns null even when found
TreeNode search(TreeNode root, int val) {
    if (root == null) return null;
    if (root.val == val) return root;
    search(root.left, val);   // Lost!
    search(root.right, val);  // Lost!
    return null;
}

The recursive calls find the node but their results are thrown away! Without capturing or returning what the recursive calls find, the function always returns null at the end. The found node gets lost in the call stack and never bubbles up to the caller.

// CORRECT: Return the recursive result
TreeNode search(TreeNode root, int val) {
    if (root == null) return null;
    if (root.val == val) return root;
    TreeNode left = search(root.left, val);
    if (left != null) return left;
    return search(root.right, val);
}

Capture the left subtree result in a variable. If found (not null), return it immediately - no need to search right. Otherwise, return whatever the right subtree search finds (could be null or the node). This "early return" pattern prevents unnecessary work.

3
Modifying List Without Copy
// WRONG: All paths point to same list!
void findPaths(TreeNode node, List path, 
               List> result) {
    if (node == null) return;
    path.add(node.val);
    if (isLeaf(node)) {
        result.add(path);  // DANGER: reference!
    }
    findPaths(node.left, path, result);
    findPaths(node.right, path, result);
    path.remove(path.size() - 1);
}

Adding the path directly stores a reference, not a copy. As the recursion continues and modifies the same path list, all previously stored "paths" in result change too! At the end, every entry in result points to the same empty list after all backtracking completes.

// CORRECT: Add a copy of the path
if (isLeaf(node)) {
    result.add(new ArrayList<>(path));  // Copy!
}

Creating a new ArrayList with the current path contents makes an independent snapshot. Future modifications to the original path won't affect this stored copy. This is essential whenever you need to preserve the current state of a mutable collection during backtracking algorithms.

4
BFS Level Confusion
// WRONG: Mixes nodes from different levels
while (!queue.isEmpty()) {
    TreeNode node = queue.poll();
    currentLevel.add(node.val);
    // Where does one level end?
    if (node.left != null) 
        queue.offer(node.left);
    if (node.right != null) 
        queue.offer(node.right);
}

Without tracking level boundaries, you can't tell where one level ends and the next begins. Children are added to the queue immediately, so the queue constantly mixes current level nodes with next level nodes. There's no way to group nodes by their depth in the tree.

// CORRECT: Capture size BEFORE loop
while (!queue.isEmpty()) {
    int levelSize = queue.size();  // Snapshot!
    for (int i = 0; i < levelSize; i++) {
        TreeNode node = queue.poll();
        currentLevel.add(node.val);
        if (node.left != null) 
            queue.offer(node.left);
        if (node.right != null) 
            queue.offer(node.right);
    }
    result.add(currentLevel);
}

Take a snapshot of queue size BEFORE processing - this tells you exactly how many nodes are at the current level. The inner for-loop processes exactly that many nodes. Children added during processing don't affect levelSize, so they wait for the next iteration.

5
Stack Push Order in Preorder
// WRONG: Right child processed first!
Stack stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
    TreeNode node = stack.pop();
    result.add(node.val);
    if (node.left != null) 
        stack.push(node.left);
    if (node.right != null) 
        stack.push(node.right);
}
// Output: Root, Right, Left (wrong!)

Stacks are LIFO (Last In, First Out). If you push left then right, right goes on top and gets popped first! This produces Root-Right-Left order instead of the correct Preorder which is Root-Left-Right. The push order must be reversed from what you want to process.

// CORRECT: Push right FIRST so left pops first
if (node.right != null) 
    stack.push(node.right);
if (node.left != null) 
    stack.push(node.left);
// Output: Root, Left, Right (correct!)

Push right child first, then left child. Since stack is LIFO, left child (pushed last) will be popped and processed first. This gives the correct preorder: Root → Left subtree → Right subtree. Remember: push in REVERSE order of desired processing!

6
Height vs Depth Confusion
// Height of null = -1, single node = 0
int heightV1(TreeNode root) {
    if (root == null) return -1;
    return 1 + Math.max(heightV1(root.left), 
                        heightV1(root.right));
}

This convention counts edges, not nodes. An empty tree has height -1, a single node has height 0 (zero edges to travel). Some textbooks and academic sources prefer this definition because height represents the longest path length (number of edges) from root to leaf.

// Height of null = 0, single node = 1
int heightV2(TreeNode root) {
    if (root == null) return 0;
    return 1 + Math.max(heightV2(root.left), 
                        heightV2(root.right));
}
// TIP: LeetCode typically uses this!

This convention counts nodes along the path. An empty tree has height 0, a single node has height 1. LeetCode and most coding interviews use this definition. Always read the problem statement carefully to determine which convention to use - test with the examples provided!

09

Time and Space Complexity

Understanding complexity is crucial for interviews. Know these by heart!

Complexity Analysis for Trees

For trees with n nodes and height h: most operations visit each node once giving O(n) time. Space complexity is typically O(h) for recursion stack, which is O(log n) for balanced trees and O(n) for skewed trees.

Operation Time Complexity Space Complexity Notes
DFS Traversal O(n) O(h) Visit each node once, stack depth = height
BFS Traversal O(n) O(w) w = max width, worst case O(n/2) for perfect tree
Search in BST O(h) O(h) O(log n) if balanced, O(n) if skewed
Insert in BST O(h) O(h) Same as search
Delete in BST O(h) O(h) Find + restructure
Morris Traversal O(n) O(1) No stack, uses threaded pointers
Build from Traversals O(n) O(n) With HashMap for O(1) index lookup
LCA (Binary Tree) O(n) O(h) Must check all nodes in worst case
LCA (BST) O(h) O(h) Use BST property to guide search
Diameter O(n) O(h) Single pass with height calculation

Height in Different Tree Types

// Height Analysis
// n = number of nodes, h = height

/*
 * BALANCED BINARY TREE (AVL, Red-Black):
 *     Height h = O(log n)
 *     Example: n=7 nodes → h=2
 *         1
 *       /   \
 *      2     3
 *     / \   / \
 *    4   5 6   7
 * 
 * COMPLETE BINARY TREE:
 *     Height h = floor(log₂(n))
 *     All levels full except possibly last
 * 
 * SKEWED TREE (worst case):
 *     Height h = O(n)
 *     Example: n=4 nodes → h=3
 *     1
 *      \
 *       2
 *        \
 *         3
 *          \
 *           4
 * 
 * PERFECT BINARY TREE:
 *     Nodes n = 2^(h+1) - 1
 *     Height h = log₂(n+1) - 1
 */

// Quick reference:
// Balanced: h ≈ log₂(n)
// Skewed: h = n - 1
// Perfect with h levels: n = 2^(h+1) - 1

Space Complexity Deep Dive

// Recursive calls use stack space
// Stack depth = height of tree = h

// Example: Counting nodes recursively
int count(TreeNode root) {
    if (root == null) return 0;
    return 1 + count(root.left) + count(root.right);
}
// Time: O(n) - visit every node
// Space: O(h) - recursion stack

// For balanced tree (h = log n): Space = O(log n)
// For skewed tree (h = n):       Space = O(n)

// BFS space depends on WIDTH, not height
List> levelOrder(TreeNode root) {
    // Queue stores at most one level at a time
    // Max width of perfect tree at level h = 2^h
    // For tree with n nodes, max width ≈ n/2
    // Space: O(n/2) = O(n) worst case
}

// Iterative DFS uses explicit stack
// Space: O(h) same as recursive

Practice Questions: Templates and Patterns

Task: Count the number of leaf nodes (nodes with no children) in a binary tree.

Show Solution
int countLeaves(TreeNode root) {
    if (root == null) return 0;
    
    // Leaf: no children
    if (root.left == null && root.right == null) {
        return 1;
    }
    
    // Internal node: count leaves in subtrees
    return countLeaves(root.left) + countLeaves(root.right);
}

// Example:     1
//            /   \
//           2     3
//          / \
//         4   5
// Leaves: 4, 5, 3 → Count = 3

Task: Return a list of all leaf node values from left to right.

Show Solution
List getLeafValues(TreeNode root) {
    List leaves = new ArrayList<>();
    collectLeaves(root, leaves);
    return leaves;
}

void collectLeaves(TreeNode node, List leaves) {
    if (node == null) return;
    
    if (node.left == null && node.right == null) {
        leaves.add(node.val);
        return;
    }
    
    collectLeaves(node.left, leaves);
    collectLeaves(node.right, leaves);
}

// For tree:    1
//            /   \
//           2     3
//          / \     \
//         4   5     6
// Output: [4, 5, 6]

Task: Return the values visible from the left side of the tree (first node at each level).

Show Solution
// BFS Approach
List leftSideView(TreeNode root) {
    List result = new ArrayList<>();
    if (root == null) return result;
    
    Queue queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            if (i == 0) {  // First node in level
                result.add(node.val);
            }
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
    }
    return result;
}

// DFS Approach (track max level seen)
List result = new ArrayList<>();

List leftSideViewDFS(TreeNode root) {
    result.clear();
    dfs(root, 0);
    return result;
}

void dfs(TreeNode node, int level) {
    if (node == null) return;
    
    if (level == result.size()) {
        result.add(node.val);  // First node at this level
    }
    
    dfs(node.left, level + 1);   // Go left first!
    dfs(node.right, level + 1);
}

Task: Two trees are leaf-similar if their leaf value sequence (left to right) is the same. Check if given trees are leaf-similar.

Show Solution
boolean leafSimilar(TreeNode root1, TreeNode root2) {
    List leaves1 = new ArrayList<>();
    List leaves2 = new ArrayList<>();
    
    collectLeaves(root1, leaves1);
    collectLeaves(root2, leaves2);
    
    return leaves1.equals(leaves2);
}

void collectLeaves(TreeNode node, List leaves) {
    if (node == null) return;
    if (node.left == null && node.right == null) {
        leaves.add(node.val);
        return;
    }
    collectLeaves(node.left, leaves);
    collectLeaves(node.right, leaves);
}

// Tree1:      3          Tree2:      3
//            / \                    / \
//           5   1                  5   1
//          / \                    / \
//         6   2                  6   7
// Leaves1: [6, 2, 1]    Leaves2: [6, 7, 1]
// Not leaf-similar!

Task: Return values grouped by vertical columns (root is column 0, left is -1, right is +1).

Show Solution
List> verticalOrder(TreeNode root) {
    List> result = new ArrayList<>();
    if (root == null) return result;
    
    Map> columnMap = new TreeMap<>();
    Queue nodeQueue = new LinkedList<>();
    Queue colQueue = new LinkedList<>();
    
    nodeQueue.offer(root);
    colQueue.offer(0);
    
    while (!nodeQueue.isEmpty()) {
        TreeNode node = nodeQueue.poll();
        int col = colQueue.poll();
        
        columnMap.computeIfAbsent(col, k -> new ArrayList<>()).add(node.val);
        
        if (node.left != null) {
            nodeQueue.offer(node.left);
            colQueue.offer(col - 1);
        }
        if (node.right != null) {
            nodeQueue.offer(node.right);
            colQueue.offer(col + 1);
        }
    }
    
    result.addAll(columnMap.values());
    return result;
}

// Example:      3
//             /   \
//            9     20
//                 /  \
//               15    7
// Columns: {-1:[9], 0:[3,15], 1:[20], 2:[7]}
// Output: [[9], [3, 15], [20], [7]]

Task: Return the boundary values of the tree in anti-clockwise direction starting from root: left boundary, leaves, right boundary (reversed).

Show Solution
List boundaryOfBinaryTree(TreeNode root) {
    List result = new ArrayList<>();
    if (root == null) return result;
    
    if (!isLeaf(root)) result.add(root.val);
    
    // Left boundary (top to bottom, exclude leaves)
    addLeftBoundary(root.left, result);
    
    // All leaves (left to right)
    addLeaves(root, result);
    
    // Right boundary (bottom to top, exclude leaves)
    addRightBoundary(root.right, result);
    
    return result;
}

boolean isLeaf(TreeNode node) {
    return node != null && node.left == null && node.right == null;
}

void addLeftBoundary(TreeNode node, List result) {
    while (node != null) {
        if (!isLeaf(node)) result.add(node.val);
        node = (node.left != null) ? node.left : node.right;
    }
}

void addRightBoundary(TreeNode node, List result) {
    List temp = new ArrayList<>();
    while (node != null) {
        if (!isLeaf(node)) temp.add(node.val);
        node = (node.right != null) ? node.right : node.left;
    }
    Collections.reverse(temp);
    result.addAll(temp);
}

void addLeaves(TreeNode node, List result) {
    if (node == null) return;
    if (isLeaf(node)) {
        result.add(node.val);
        return;
    }
    addLeaves(node.left, result);
    addLeaves(node.right, result);
}
11

Visual Reference Guide

Quick visual reference for common tree operations. Print this out or bookmark for quick review!

ASCII Art Reference

Sometimes the best way to understand trees is to see them drawn out. These ASCII diagrams show common tree patterns you will encounter in problems.

Tree Types Comparison

Full Binary Tree
0 or 2 children only
1 2 3 4 5
Every node: 0 or 2 children No single child nodes
Complete Binary Tree
Filled left to right
1 2 3 4 5 6
All levels filled except last Used in Heaps
Perfect Binary Tree
All leaves same level
1 2 3 4 5 6 7
Nodes = 2^(h+1) - 1 Fully symmetric
Left Skewed
height = n - 1
1 2 3 4
Worst case O(n) Like a linked list
Right Skewed
height = n - 1
1 2 3 4
Worst case O(n) Like a linked list
Balanced Tree
height = log(n)
4 2 6 1 3 5 7
Optimal O(log n) AVL, Red-Black

Traversal Patterns

Example Tree 1 2 3 4 5
PREORDER
Root → Left → Right
1 2 4 5 3
Used for: Cloning, Serialization, Prefix expressions
INORDER
Left → Root → Right
4 2 5 1 3
Used for: BST sorted output, Infix expressions
POSTORDER
Left → Right → Root
4 5 2 3 1
Used for: Deletion, Postfix expressions, Dependencies
LEVEL-ORDER (BFS)
Level by Level
1 2 3 4 5
Used for: Shortest path, Width calculation

BFS Queue Visualization

BFS Queue Step-by-Step
Level-order traversal using Queue
1 2 3 4 5 6 L0 L1 L2
Step 0 Initialize queue
Queue:1
Step 1 Poll 1, Add children 2, 3
Queue:23
Step 2 Poll 2, Add children 4, 5
Queue:345
Step 3 Poll 3, Add child 6
Queue:456
Step 4 Poll 4 (leaf node)
Queue:56
Step 5 Poll 5 (leaf node)
Queue:6
Step 6 Poll 6 (leaf) → Done!
[ empty ]
Level-Order Output
[[1], [2, 3], [4, 5, 6]]
KEY: Use queue.size() BEFORE the inner loop to know level boundaries!

Stack for DFS Visualization

DFS with Stack (Preorder)
Iterative depth-first traversal
1 2 3 4 5
Step 0 Initialize stack with root
1
[ ]
Step 1 Pop 1, Push 3, 2 (right first!)
23
[1]
Step 2 Pop 2, Push 5, 4
453
[1, 2]
Step 3 Pop 4 (leaf node)
53
[1, 2, 4]
Step 4 Pop 5 (leaf node)
3
[1, 2, 4, 5]
Step 5 Pop 3 (leaf) → Done!
[ ]
[1, 2, 4, 5, 3]
Preorder Output
[1, 2, 4, 5, 3]
KEY: Push RIGHT child first so LEFT is popped first (Stack is LIFO)!

Recursion Call Stack

Recursion Call Stack (Inorder)
How the call stack evolves during recursion
1 2 3
void inorder(node):
  if null: return
  inorder(left)
  // print(node.val)
  inorder(right)
Call Stack Evolution
inorder(1)
inorder(2)
going left
inorder(null)
return
OUTPUT: 2
inorder(null)
return
OUTPUT: 2, 1
inorder(3)
going right
inorder(null)
return
OUTPUT: 2, 1, 3
Space Complexity
O(h)
h = height of tree
Stack depth = max recursion depth

Common Tree Formulas

Tree Math Formulas
Essential formulas for binary trees with n nodes and height h
Max nodes at level k
2k
L0: 1L1: 2L2: 4L3: 8
Total nodes in PERFECT tree
2h+1 - 1
h=0: 1h=1: 3h=2: 7h=3: 15
Min height for n nodes
floor(log₂(n))
Achieved by complete/balanced tree
Max height for n nodes
n - 1
Achieved by skewed tree (worst case)
Leaves in FULL binary tree
(n + 1) / 2
Internal nodes: (n - 1) / 2
Leaf-Internal Relation
L = I + 1
L = leaves, I = internal nodes
Complete Binary Tree (Heap) - Array Index Formulas
Parent of i
(i - 1) / 2
Left Child
2 * i + 1
Right Child
2 * i + 2

Bonus Practice: Mixed Difficulty

Task: Given two binary trees, merge them by summing overlapping nodes. If one tree is missing a node, use the other tree's node.

Show Solution
TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
    if (t1 == null) return t2;
    if (t2 == null) return t1;
    
    // Both non-null: create merged node
    TreeNode merged = new TreeNode(t1.val + t2.val);
    merged.left = mergeTrees(t1.left, t2.left);
    merged.right = mergeTrees(t1.right, t2.right);
    
    return merged;
}

// Tree1:     1          Tree2:     2
//           / \                   / \
//          3   2                 1   3
//         /                       \   \
//        5                         4   7
//
// Merged:    3
//           / \
//          4   5
//         / \   \
//        5   4   7

Task: Count paths that sum to target. Path can start and end anywhere (not necessarily root to leaf), but must go downward.

Show Solution
int pathSum(TreeNode root, int targetSum) {
    if (root == null) return 0;
    
    // Paths starting from root + paths in subtrees
    return countFromNode(root, targetSum) + 
           pathSum(root.left, targetSum) + 
           pathSum(root.right, targetSum);
}

int countFromNode(TreeNode node, long remaining) {
    if (node == null) return 0;
    
    int count = 0;
    if (node.val == remaining) count++;  // Found one path!
    
    count += countFromNode(node.left, remaining - node.val);
    count += countFromNode(node.right, remaining - node.val);
    
    return count;
}

// O(n^2) worst case. Can optimize to O(n) with prefix sum HashMap!

Task: Given a target node and distance K, return all nodes that are K edges away from target (can go up to parent).

Show Solution
List distanceK(TreeNode root, TreeNode target, int k) {
    Map parentMap = new HashMap<>();
    buildParentMap(root, null, parentMap);
    
    // BFS from target
    Queue queue = new LinkedList<>();
    Set visited = new HashSet<>();
    queue.offer(target);
    visited.add(target);
    
    int distance = 0;
    while (!queue.isEmpty()) {
        if (distance == k) {
            List result = new ArrayList<>();
            for (TreeNode node : queue) result.add(node.val);
            return result;
        }
        
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            TreeNode node = queue.poll();
            
            // Add neighbors: left, right, parent
            if (node.left != null && !visited.contains(node.left)) {
                visited.add(node.left);
                queue.offer(node.left);
            }
            if (node.right != null && !visited.contains(node.right)) {
                visited.add(node.right);
                queue.offer(node.right);
            }
            TreeNode parent = parentMap.get(node);
            if (parent != null && !visited.contains(parent)) {
                visited.add(parent);
                queue.offer(parent);
            }
        }
        distance++;
    }
    return new ArrayList<>();
}

void buildParentMap(TreeNode node, TreeNode parent, Map map) {
    if (node == null) return;
    map.put(node, parent);
    buildParentMap(node.left, node, map);
    buildParentMap(node.right, node, map);
}

Task: Count nodes in a COMPLETE binary tree in better than O(n) time. Use the complete tree property!

Show Solution
int countNodes(TreeNode root) {
    if (root == null) return 0;
    
    int leftHeight = getLeftHeight(root);
    int rightHeight = getRightHeight(root);
    
    if (leftHeight == rightHeight) {
        // Perfect tree: 2^h - 1 nodes
        return (1 << leftHeight) - 1;
    } else {
        // Not perfect: count recursively
        return 1 + countNodes(root.left) + countNodes(root.right);
    }
}

int getLeftHeight(TreeNode node) {
    int h = 0;
    while (node != null) {
        h++;
        node = node.left;
    }
    return h;
}

int getRightHeight(TreeNode node) {
    int h = 0;
    while (node != null) {
        h++;
        node = node.right;
    }
    return h;
}

// Time: O(log^2 n) because:
// - Height comparison is O(log n)
// - We recurse O(log n) times
// - Each recursion does O(log n) height calculation

Task: Two nodes in a BST were swapped by mistake. Recover the tree without changing its structure.

Show Solution
TreeNode first = null, second = null, prev = null;

void recoverTree(TreeNode root) {
    first = second = prev = null;
    inorder(root);
    
    // Swap values of the two wrong nodes
    int temp = first.val;
    first.val = second.val;
    second.val = temp;
}

void inorder(TreeNode node) {
    if (node == null) return;
    
    inorder(node.left);
    
    // In correct BST: prev.val < node.val always
    if (prev != null && prev.val > node.val) {
        if (first == null) {
            first = prev;  // First violation
        }
        second = node;  // Update second (might be adjacent swap)
    }
    prev = node;
    
    inorder(node.right);
}

// Example: [1, 3, 2] - nodes 3 and 2 are swapped
// Inorder should be [1, 2, 3]
// Violation: 3 > 2, so first=3, second=2
// Swap back: [1, 2, 3]
12

Quick Reference Code Snippets

Copy-paste ready code for common operations. Bookmark this section for quick access during practice!

Ready-to-Use Code

These snippets are tested and ready to use. Copy them into your IDE and modify as needed for your specific problem.

TreeNode Class (Standard)

// Standard TreeNode definition used by LeetCode
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    
    TreeNode() {}
    
    TreeNode(int val) { 
        this.val = val; 
    }
    
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

This is the standard TreeNode class used in LeetCode problems. It contains an integer value and references to left and right children. Three constructors are provided: default (no args), value-only (most common), and full constructor with children (useful for building trees inline). You don't need to memorize this - LeetCode provides it automatically in tree problems.

// TreeNode with parent pointer (sometimes needed)
class TreeNodeWithParent {
    int val;
    TreeNodeWithParent left, right, parent;
    
    TreeNodeWithParent(int val) {
        this.val = val;
    }
}

Some problems require navigating upward from child to parent. This extended TreeNode includes a parent pointer in addition to the standard left/right children. Common use cases include finding Lowest Common Ancestor (LCA) with parent pointers, or traversing from any node to the root. Remember to maintain parent pointers when inserting nodes.

// Generic TreeNode for any data type
class TreeNode {
    T val;
    TreeNode left, right;
    
    TreeNode(T val) {
        this.val = val;
    }
}

A generic version of TreeNode that can hold any data type - strings, custom objects, etc. The type parameter T is specified when creating the tree. This is useful for real-world applications where trees store complex data. For interview coding, the standard int-based TreeNode is almost always used, but understanding generics shows good Java fundamentals.

Build Tree from Array (LeetCode Format)

// Build tree from level-order array with nulls
// Input: [3, 9, 20, null, null, 15, 7]
// Creates:      3
//              / \
//             9  20
//               /  \
//              15   7

LeetCode represents trees as level-order arrays where null indicates missing children. The array is read left-to-right, top-to-bottom. Understanding this format is crucial for local testing - you can copy test cases directly from LeetCode and build trees to debug your solutions. Node 3 is root, 9 and 20 are its children, 9 has no children (null, null), and 20 has children 15 and 7.

TreeNode buildTree(Integer[] arr) {
    if (arr == null || arr.length == 0 || arr[0] == null) {
        return null;
    }
    
    TreeNode root = new TreeNode(arr[0]);
    Queue queue = new LinkedList<>();
    queue.offer(root);
    int i = 1;

We use Integer[] instead of int[] to allow null values in the array. First, handle edge cases - empty array or null root. Create the root node from the first element. Initialize a queue to track parent nodes that need children assigned, and an index i starting at 1 (since index 0 is the root). BFS-style processing ensures level-order construction.

    while (!queue.isEmpty() && i < arr.length) {
        TreeNode current = queue.poll();
        
        // Left child
        if (i < arr.length) {
            if (arr[i] != null) {
                current.left = new TreeNode(arr[i]);
                queue.offer(current.left);
            }
            i++;
        }

For each parent node dequeued, assign its left child from the next array element. If the value is null, we skip creating a node but still increment i - this maintains proper alignment. If not null, create the child node and add it to the queue so its own children can be assigned later. The bounds check ensures we don't read past the array.

        // Right child
        if (i < arr.length) {
            if (arr[i] != null) {
                current.right = new TreeNode(arr[i]);
                queue.offer(current.right);
            }
            i++;
        }
    }
    
    return root;
}

Same logic for the right child - read next element, create node if not null, add to queue. The pattern of checking i twice (left and right) ensures we process pairs correctly. After the loop completes, all nodes have been created and linked. Return the root to give access to the entire tree. This method runs in O(n) time and O(n) space.

// Usage in main:
Integer[] arr = {3, 9, 20, null, null, 15, 7};
TreeNode root = buildTree(arr);

To use this helper, simply copy the array from LeetCode's test case and call buildTree(). Now you can test your solution locally with the same tree structure. Pro tip: combine this with a printTree() method to visualize the tree and verify it matches your expectations before running your algorithm.

Print Tree (for Debugging)

// Pretty print tree structure for debugging
void printTree(TreeNode root) {
    printTreeHelper(root, "", true);
}

This is the public entry point for printing a tree visually. It delegates to a helper method that handles the recursive logic with additional parameters for formatting. The empty string is the initial prefix (indentation), and true indicates we're starting with the left branch convention. This produces a sideways tree visualization in the console.

void printTreeHelper(TreeNode node, String prefix, boolean isLeft) {
    if (node == null) {
        System.out.println(prefix + (isLeft ? "├── " : "└── ") + "null");
        return;
    }
    
    System.out.println(prefix + (isLeft ? "├── " : "└── ") + node.val);
    
    if (node.left != null || node.right != null) {
        printTreeHelper(node.left, prefix + (isLeft ? "│   " : "    "), true);
        printTreeHelper(node.right, prefix + (isLeft ? "│   " : "    "), false);
    }
}

The helper uses Unicode box-drawing characters (├── └── │) to create a visual tree structure. The prefix accumulates as we go deeper, adding either a vertical line (│) or spaces depending on whether we're in a left or right subtree. This creates proper indentation. Only recurse to children if at least one exists, avoiding unnecessary "null" prints at every leaf.

// Simpler version: print level-order with nulls
String treeToString(TreeNode root) {
    if (root == null) return "[]";
    
    StringBuilder sb = new StringBuilder("[");
    Queue queue = new LinkedList<>();
    queue.offer(root);

This method converts a tree back to the LeetCode array format - useful for comparing your tree against expected output. We use StringBuilder for efficient string concatenation. Start with the opening bracket and initialize BFS traversal with the root in the queue. This produces output like "[3, 9, 20, null, null, 15, 7]".

    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        if (node == null) {
            sb.append("null, ");
        } else {
            sb.append(node.val).append(", ");
            queue.offer(node.left);
            queue.offer(node.right);
        }
    }

Process nodes level by level. For null nodes, append "null" string. For valid nodes, append the value and enqueue both children (even if null) to maintain proper level-order format. This differs from typical BFS where we skip nulls - here we need them to represent the tree structure accurately. Each element gets a comma separator.

    // Remove trailing nulls for cleaner output
    String result = sb.toString().replaceAll("(, null)+, $", "");
    return result + "]";
}

The raw output has trailing nulls from leaf nodes' non-existent children. The regex removes consecutive ", null" entries at the end for cleaner output that matches LeetCode's format. Finally, close with the bracket. This helper is invaluable for debugging - print your tree to verify it matches the expected structure before investigating algorithm issues.

All Traversals in One Class

class TreeTraversals {
    
    // ========== DFS RECURSIVE ==========
    
    List preorderRecursive(TreeNode root) {
        List result = new ArrayList<>();
        preorderHelper(root, result);
        return result;
    }
    
    void preorderHelper(TreeNode node, List list) {
        if (node == null) return;
        list.add(node.val);           // Process FIRST
        preorderHelper(node.left, list);
        preorderHelper(node.right, list);
    }

Preorder visits Root-Left-Right: process the node FIRST, then recurse left, then right. This is like reading a book's table of contents - you see the chapter title before its sections. The helper pattern (public method + private recursive helper) keeps the API clean. Preorder is commonly used for creating a copy of the tree or serializing it.

    List inorderRecursive(TreeNode root) {
        List result = new ArrayList<>();
        inorderHelper(root, result);
        return result;
    }
    
    void inorderHelper(TreeNode node, List list) {
        if (node == null) return;
        inorderHelper(node.left, list);
        list.add(node.val);           // Process in MIDDLE
        inorderHelper(node.right, list);
    }

Inorder visits Left-Root-Right: go all the way left first, process node in the MIDDLE, then go right. For Binary Search Trees, inorder traversal produces sorted output - this is its most important property! Use inorder when you need sorted access to BST elements or when processing nodes "in order" of their position.

    List postorderRecursive(TreeNode root) {
        List result = new ArrayList<>();
        postorderHelper(root, result);
        return result;
    }
    
    void postorderHelper(TreeNode node, List list) {
        if (node == null) return;
        postorderHelper(node.left, list);
        postorderHelper(node.right, list);
        list.add(node.val);           // Process LAST
    }

Postorder visits Left-Right-Root: process children BEFORE the parent. Think of it as "bottom-up" processing - you handle all descendants first. This is essential for tree deletion (delete children before parent) and computing values that depend on subtree results (like calculating directory sizes where folder size = sum of file sizes).

    // ========== DFS ITERATIVE ==========
    
    List preorderIterative(TreeNode root) {
        List result = new ArrayList<>();
        if (root == null) return result;
        
        Stack stack = new Stack<>();
        stack.push(root);
        
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            result.add(node.val);
            
            if (node.right != null) stack.push(node.right);
            if (node.left != null) stack.push(node.left);
        }
        return result;
    }

Iterative preorder uses an explicit stack to simulate recursion. Key insight: push RIGHT child first so LEFT is popped first (LIFO). Process node immediately when popped. This is the simplest iterative traversal to understand and implement. Some interviewers specifically ask for iterative solutions to test your understanding of stack-based recursion simulation.

    List inorderIterative(TreeNode root) {
        List result = new ArrayList<>();
        Stack stack = new Stack<>();
        TreeNode curr = root;
        
        while (curr != null || !stack.isEmpty()) {
            while (curr != null) {
                stack.push(curr);
                curr = curr.left;
            }
            curr = stack.pop();
            result.add(curr.val);
            curr = curr.right;
        }
        return result;
    }

Iterative inorder is trickier - we need to go left as far as possible before processing. The inner while loop pushes all left descendants onto the stack. When we can't go left anymore, pop and process, then move to right subtree. The outer loop continues while we have nodes to explore (curr) or nodes waiting on the stack. This pattern appears in many tree problems.

    List postorderIterative(TreeNode root) {
        LinkedList result = new LinkedList<>();
        if (root == null) return result;
        
        Stack stack = new Stack<>();
        stack.push(root);
        
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            result.addFirst(node.val);  // Add to FRONT
            
            if (node.left != null) stack.push(node.left);
            if (node.right != null) stack.push(node.right);
        }
        return result;
    }

Clever trick: postorder is the reverse of a modified preorder! Instead of Root-Left-Right, we do Root-Right-Left and add to the FRONT of the result. Using LinkedList allows O(1) addFirst(). Push left before right (opposite of preorder) so right is processed first. This avoids the complexity of tracking "visited" nodes that true iterative postorder requires.

    // ========== BFS ==========
    
    List> levelOrder(TreeNode root) {
        List> result = new ArrayList<>();
        if (root == null) return result;
        
        Queue queue = new LinkedList<>();
        queue.offer(root);
        
        while (!queue.isEmpty()) {
            int size = queue.size();
            List level = new ArrayList<>();
            
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                level.add(node.val);
                
                if (node.left != null) queue.offer(node.left);
                if (node.right != null) queue.offer(node.right);
            }
            result.add(level);
        }
        return result;
    }
}

BFS/Level-order uses a Queue instead of Stack. The critical pattern: capture queue.size() BEFORE the inner loop - this tells you how many nodes are at the current level. Process exactly that many, collecting them into a level list. Children added during processing wait for the next iteration. Returns List of Lists where each inner list is one tree level. Essential for problems involving levels, depth, or horizontal distances.

Common Tree Properties

class TreeProperties {
    
    // Height (max depth)
    int height(TreeNode root) {
        if (root == null) return 0;
        return 1 + Math.max(height(root.left), height(root.right));
    }

Height/Max Depth: Returns the number of nodes on the longest path from root to any leaf. Base case: null returns 0. Recursive case: 1 (current node) + maximum of left and right subtree heights. This is the most fundamental tree property and appears in nearly every tree problem. Time: O(n), Space: O(h) for recursion stack.

    // Count nodes
    int count(TreeNode root) {
        if (root == null) return 0;
        return 1 + count(root.left) + count(root.right);
    }

Count Nodes: Returns total number of nodes in the tree. Same pattern as height: base case null returns 0, otherwise count current node (1) plus all nodes in left and right subtrees. This is the simplest example of "combine results from both subtrees" pattern that appears in many tree problems like finding all paths or validating properties.

    // Sum of all values
    int sum(TreeNode root) {
        if (root == null) return 0;
        return root.val + sum(root.left) + sum(root.right);
    }

Sum of Values: Returns the sum of all node values in the tree. Instead of counting nodes (adding 1), we add the node's actual value. This pattern extends to computing any aggregate: product, XOR, string concatenation, etc. Useful for problems like "path sum" where you track cumulative values along tree paths.

    // Maximum value
    int max(TreeNode root) {
        if (root == null) return Integer.MIN_VALUE;
        return Math.max(root.val, Math.max(max(root.left), max(root.right)));
    }

Maximum Value: Returns the largest value in the tree. Key insight: return Integer.MIN_VALUE for null so it never "wins" the max comparison. Compare current node's value against max of both subtrees using nested Math.max(). Note: For a BST, max is always the rightmost node (no need to check left subtree).

    // Minimum value
    int min(TreeNode root) {
        if (root == null) return Integer.MAX_VALUE;
        return Math.min(root.val, Math.min(min(root.left), min(root.right)));
    }

Minimum Value: Returns the smallest value in the tree. Mirror of max: return Integer.MAX_VALUE for null so it never "wins" the min comparison. For a BST, min is always the leftmost node - just keep going left until node.left is null. This simple traversal is used in BST deletion to find the inorder successor.

    // Is balanced?
    boolean isBalanced(TreeNode root) {
        return checkHeight(root) != -1;
    }
    
    int checkHeight(TreeNode node) {
        if (node == null) return 0;
        
        int left = checkHeight(node.left);
        if (left == -1) return -1;
        
        int right = checkHeight(node.right);
        if (right == -1) return -1;
        
        if (Math.abs(left - right) > 1) return -1;
        
        return 1 + Math.max(left, right);
    }

Is Balanced: A tree is balanced if left and right subtree heights differ by at most 1 AT EVERY NODE. Naive approach checks height at each node = O(n²). This optimized O(n) solution uses -1 as a "flag" to indicate imbalance. Once any subtree is unbalanced, -1 propagates up immediately (early termination). This pattern of using return value for both result and error signaling is common in interviews.

    // Is symmetric?
    boolean isSymmetric(TreeNode root) {
        if (root == null) return true;
        return isMirror(root.left, root.right);
    }
    
    boolean isMirror(TreeNode t1, TreeNode t2) {
        if (t1 == null && t2 == null) return true;
        if (t1 == null || t2 == null) return false;
        return t1.val == t2.val && 
               isMirror(t1.left, t2.right) && 
               isMirror(t1.right, t2.left);
    }

Is Symmetric: A tree is symmetric if it's a mirror of itself. The key insight is comparing TWO nodes at a time: left.left with right.right, and left.right with right.left (cross comparison). Three conditions: both null = symmetric, one null = not symmetric, both exist = check values match AND children are mirrors. This "two-pointer on trees" pattern also appears in "same tree" and "subtree" problems.

    // Diameter (longest path)
    int diameter = 0;
    
    int diameterOfBinaryTree(TreeNode root) {
        diameter = 0;
        depth(root);
        return diameter;
    }
    
    int depth(TreeNode node) {
        if (node == null) return 0;
        int left = depth(node.left);
        int right = depth(node.right);
        diameter = Math.max(diameter, left + right);
        return 1 + Math.max(left, right);
    }
}

Diameter (Longest Path): The diameter is the longest path between ANY two nodes (may not pass through root). At each node, the longest path THROUGH that node = left depth + right depth. We use a class variable to track the global maximum across all nodes. The helper returns depth (for parent's calculation) while updating diameter as a side effect. This "return one thing, update another" pattern is common for tree path problems.

Complete Solution Template

import java.util.*;

class Solution {
    // Your tree algorithm here
}

Imports & Class Setup: Always import java.util.* for tree problems - you'll need ArrayList, LinkedList, Queue, Stack, HashMap, etc. The Solution class is LeetCode's standard format. Place your algorithm methods inside this class. For local testing, you can add a main method, but remove it before submitting to LeetCode.

    public static void main(String[] args) {
        // Build test tree
        //       1
        //      / \
        //     2   3
        //    / \
        //   4   5
        
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.left = new TreeNode(4);
        root.left.right = new TreeNode(5);

Building Test Trees: Draw your tree structure in comments first - this helps visualize and debug. Build trees manually by creating root, then attaching children with root.left = new TreeNode(val). For complex trees, use the buildTree helper from earlier. This 5-node tree covers common cases: root with two children, left child with two children, right child as leaf.

        Solution sol = new Solution();
        
        // Test your solution
        System.out.println("Testing...");
        
        // Example: test height
        // System.out.println("Height: " + sol.maxDepth(root));
        
        // Example: test traversal
        // System.out.println("Inorder: " + sol.inorderTraversal(root));
    }

Testing Your Solution: Create a Solution instance and call your methods. Print results to verify correctness. Test multiple cases: empty tree (null), single node, full tree, skewed tree (all left or all right). Compare output against hand-traced expected values. Uncomment and modify the example lines based on which method you're implementing.

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

TreeNode Class: This is the standard LeetCode TreeNode definition - memorize it! Three constructors: empty (rarely used), value-only (most common), and full (useful for building trees inline like new TreeNode(1, new TreeNode(2), new TreeNode(3))). On LeetCode, this class is pre-defined - don't include it in submissions. For local testing, add it outside your Solution class.

Key Takeaways

Binary Tree = 2 Children Max

Each node has at most left and right child pointers.

DFS = Stack (Recursion)

Inorder (L-Root-R), Preorder (Root-L-R), Postorder (L-R-Root).

BFS = Queue

Level-order traversal processes nodes level by level.

Inorder of BST = Sorted

Inorder traversal of BST gives elements in sorted order.

Balanced = O(log n) Height

Balanced trees ensure efficient operations by limiting height.

Recursion is Key

Most tree problems have elegant recursive solutions with base case at null.

Knowledge Check

Question 1 of 6

What traversal gives sorted output for a BST?

Question 2 of 6

What data structure is used for level-order traversal?

Question 3 of 6

A node with no children is called?

Question 4 of 6

What is the preorder sequence: Root→Left→Right for tree with root 1, left child 2, right child 3?

Question 5 of 6

What is the height of a tree with only a root node?

Question 6 of 6

In a binary tree, the maximum number of nodes at level k is?