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.
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;
}
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;
}
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)
}
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.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;
}
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);
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)
);
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:
- TreeNode class: Each node stores a value (
val), and two pointers (left,right) to child nodes - Constructor 1: Creates a node with just a value, children default to
null - Constructor 2: Creates a node with value AND pre-built left/right subtrees
- Manual building: Create root first, then attach children via
.leftand.right - 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;
}
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);
}
// Preorder: Root → Left → Right
void preorder(TreeNode root) {
if (root == null) return;
System.out.print(root.val + " ");
preorder(root.left);
preorder(root.right);
}
// Postorder: Left → Right → Root
void postorder(TreeNode root) {
if (root == null) return;
postorder(root.left);
postorder(root.right);
System.out.print(root.val + " ");
}
// 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
Traversal Order Visualization
✓ BST = sorted output!
✓ Copy/clone trees
✓ 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;
}
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 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;
}
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!
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;
}
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]]
[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;
}
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
[3]
→ Poll 3, add children
[9, 20]
→ Poll both, add 15, 7
[15, 7]
→ Poll both, no children
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;
}
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));
}
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));
}
// Count Total Nodes
int countNodes(TreeNode root) {
if (root == null) return 0;
return 1 + countNodes(root.left) + countNodes(root.right);
}
// 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);
}
// 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);
}
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);
}
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
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
}
// 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);
}
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;
}
// 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
}
// 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
}
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);
}
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
}
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);
}
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));
}
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));
}
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);
}
// 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);
}
&& 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);
}
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);
}
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;
}
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;
}
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;
}
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!
}
// 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);
}
|| 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
}
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);
}
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);
}
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;
}
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;
}
[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);
}
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;
}
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
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.
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.
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.
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.
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.
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.
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!
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!
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);
}
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
Complete Binary Tree
Perfect Binary Tree
Left Skewed
Right Skewed
Balanced Tree
Traversal Patterns
BFS Queue Visualization
queue.size() BEFORE the inner loop to know level boundaries!
Stack for DFS Visualization
Recursion Call Stack
if null: return
inorder(left)
// print(node.val)
inorder(right)
Common Tree Formulas
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]
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
What traversal gives sorted output for a BST?
What data structure is used for level-order traversal?
A node with no children is called?
What is the preorder sequence: Root→Left→Right for tree with root 1, left child 2, right child 3?
What is the height of a tree with only a root node?
In a binary tree, the maximum number of nodes at level k is?