Tree Interview Problems — Stack Overflow on 50k-Node Trees
StackOverflowError from recursive maxDepth on 50k-node degenerate tree? Avoid recursive DFS — proven production fix that beats memorization..
20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- Tree interview problems follow 5 core patterns: DFS, BFS, two-pointer, post-order gather, and pre-order serialization
- Recursive DFS is clean but risks stack overflow on deep trees, use iterative stack for production code
- BFS uses Queue and is best for level-order, shortest path, and right-side view
- Two-pointer technique (e.g., isSameTree) checks structural identity, not just value equality
- Biggest mistake: assuming a Binary Tree is a BST — never use sortedness without verifying the property
Tree interview problems are a staple of technical interviews at companies like Google, Meta, and Amazon, but most practice materials stop at toy examples with 10–20 nodes. In production, you routinely encounter trees with 50,000+ nodes — think DOM trees in a browser, file system directories, or ASTs from large codebases.
The challenge isn't just knowing recursion; it's understanding how stack depth, memory pressure, and traversal order behave when your tree fills a significant chunk of L2 cache. This article covers the five patterns that actually matter at scale: DFS for recursive bottom-up work, BFS for level-order processing, two-pointer techniques for structural comparison without cloning, post-order gather-then-decide for aggregation problems like subtree sums, and pre-order serialization for building trees from wire formats.
Each pattern is analyzed for its stack behavior, memory footprint, and iteration vs. recursion tradeoffs when node counts hit 50k. You'll learn why a naive recursive DFS can blow your call stack on a skewed tree, how BFS with a queue can OOM if you don't bound your levels, and when two-pointer tree walking beats hash-based comparison.
This isn't theory — these are the patterns you'll debug at 2 AM when your tree traversal silently kills your production process.
Imagine a company org chart: the CEO is at the top, managers branch out below them, and individual contributors sit at the leaves. A tree data structure works exactly like that — one root, branches splitting off, ending in leaves with no children. Nearly every hard interview problem that looks complicated is actually just asking you to navigate or compare parts of that org chart in a clever way. Once you see the shape of the problem, the solution almost writes itself.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Trees show up in interviews more than almost any other data structure — and for good reason. File systems, DOM trees, database indexes (B-trees), routing tables, and compiler parse trees are all trees in disguise. A candidate who can fluently navigate a tree signals that they understand recursion, stack/queue trade-offs, and hierarchical thinking. That is a rare combination, and interviewers know it.
The frustrating part is that most candidates memorize solutions instead of patterns. They grind LeetCode until they've seen 'invert binary tree' a hundred times, but the moment an interviewer tweaks the problem slightly — 'now do it iteratively' or 'what if nodes have parent pointers?' — they freeze. The real skill isn't knowing answers, it's recognizing which of the five or six fundamental tree patterns applies to a new problem you've never seen before.
By the end of this article you'll have a mental toolkit of those patterns: DFS vs BFS trade-offs, the two-pointer trick adapted for trees, the post-order 'gather-then-decide' approach, and more. Every problem below is chosen because it directly teaches a transferable pattern. Work through the code, tweak the inputs, break it — that's how the pattern becomes muscle memory.
How Tree Interview Problems Scale to 50k-Node Trees
Tree interview problems test your ability to navigate hierarchical data structures — binary trees, BSTs, tries, or N-ary trees — using recursion or iterative traversal. The core mechanic is choosing the right traversal order (preorder, inorder, postorder, level-order) to solve a problem like subtree checking, path sum, or serialization. At 50k nodes, recursion depth can blow the stack (default Java stack ~1MB, ~10k frames), so iterative approaches or tail-recursion optimization become mandatory.
Key properties: tree problems are O(n) in time and O(h) in space for recursion (h = height). Balanced trees (height ~log n) are safe; skewed trees (height = n) cause stack overflow. In practice, 50k nodes in a degenerate BST means 50k recursive calls — a guaranteed crash. Iterative traversal with explicit stacks or parent pointers avoids this, but adds complexity.
Use tree problems when data has inherent hierarchy — file systems, DOM, ASTs, routing tables. They matter because real systems process millions of nodes: compilers parse ASTs, databases traverse B-trees, and UI frameworks diff virtual DOMs. Understanding traversal trade-offs prevents silent failures in production.
Pattern 1: Depth-First Search (DFS) & Recursive Bottom-Up
Depth-First Search is the bread and butter of tree manipulation. In a recursive bottom-up approach, you process children first and then return information to the parent. This is essential for problems like calculating the 'Maximum Depth' or 'Diameter of a Binary Tree.' By thinking of each node as the 'root' of its own sub-tree, you build a solution for the entire tree through sub-problem results.
Recursive DFS is elegant but risks stack overflow on deep trees — the JVM default stack size is about 1 MB, and each recursion frame costs ~48 bytes (plus object overhead). A degenerate tree with 20,000 nodes will overflow. Always ask the interviewer about the tree size before committing to recursion.
package io.thecodeforge.trees; /** * io.thecodeforge: Efficiently calculating the maximum depth of a Binary Tree. * Pattern: Recursive DFS (Post-order) */ public class DepthCalculator { public int maxDepth(TreeNode root) { // Base Case: An empty tree has depth 0 if (root == null) { return 0; } // Recursive Step: Gather information from children int leftDepth = maxDepth(root.left); int rightDepth = maxDepth(root.right); // Combine Step: Current depth is max of children + 1 (itself) return Math.max(leftDepth, rightDepth) + 1; } } class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; } }
root == null) or you'll hit a StackOverflowError.Pattern 2: Breadth-First Search (BFS) & Level-Order Traversal
When an interviewer asks for 'Level Order Traversal' or 'Finding the Right Side View,' you need Breadth-First Search. Unlike DFS, BFS uses a Queue to explore the tree layer-by-layer. This is functionally different from recursion because it explores horizontal relationships before vertical ones.
BFS is also the go-to for shortest path problems in trees (e.g., min depth to a leaf). The space complexity is O(W) where W is maximum width of the tree — in a perfect binary tree, W can be N/2, which is much larger than DFS's O(height). For a chain tree, BFS uses O(1) memory while DFS uses O(N). So there's no universal winner.
package io.thecodeforge.trees; import java.util.*; public class LevelExplorer { /** * io.thecodeforge: Standard Level-Order Traversal implementation. */ public List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> result = new ArrayList<>(); if (root == null) return result; Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { int levelSize = queue.size(); List<Integer> 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; } }
Pattern 3: Two-Pointer Technique for Trees (Structural Comparison)
The two-pointer technique, common in arrays and linked lists, adapts beautifully to trees. Problems like 'Same Tree', 'Symmetric Tree', and 'Subtree of Another Tree' ask you to compare two trees simultaneously. The pattern: traverse both trees in lockstep, comparing each node and its children.
For 'Same Tree', you recursively (or iteratively) check if both current nodes are null (equal), both non-null with same value, and then recursively check left and right children. The base case must handle nulls correctly—most mistakes come from failing to consider that one tree might have a null where the other doesn't.
For 'Symmetric Tree', you compare the left subtree with the flipped right subtree. A common trick: convert the problem into a 'Same Tree' comparison where you compare the original tree's left with its own right (mirrored).
package io.thecodeforge.trees; public class TreeComparison { // Pattern: Two-pointer recursion — compare two nodes simultaneously public boolean isSameTree(TreeNode p, TreeNode q) { if (p == null && q == null) return true; if (p == null || q == null) return false; if (p.val != q.val) return false; return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); } public boolean isSymmetric(TreeNode root) { if (root == null) return true; return isMirror(root.left, root.right); } private boolean isMirror(TreeNode left, TreeNode right) { if (left == null && right == null) return true; if (left == null || right == null) return false; return (left.val == right.val) && isMirror(left.left, right.right) && isMirror(left.right, right.left); } }
both null first, then one null. If you check if (p == null || q == null) before the both-null check, you'll return false prematurely when both are null.Pattern 4: Post-Order Gather-Then-Decide (Bottom-Up Aggregation)
Some problems require each node to collect information from its children, then decide a property for itself. Classic examples: 'Diameter of Binary Tree', 'Maximum Path Sum', 'Balanced Binary Tree Check'. The pattern: compute a value for the subtree (e.g., height), then combine children's results to determine something about the current node (e.g., diameter = max(diameter, leftHeight + rightHeight)).
This is distinct from simple depth calculation because you're not just returning a single value to the parent—you're also maintaining a global or passed-by-reference variable for the overall answer. In 'Diameter of a Binary Tree', the function returns height but updates a max diameter. In 'Maximum Path Sum', the function returns the maximum path sum starting from that node downwards, while a global variable tracks the max path that goes through the node.
package io.thecodeforge.trees; public class BottomUpAggregator { private int diameter = 0; public int diameterOfBinaryTree(TreeNode root) { height(root); return diameter; } private int height(TreeNode node) { if (node == null) return 0; int leftHeight = height(node.left); int rightHeight = height(node.right); // Update diameter: longest path through this node diameter = Math.max(diameter, leftHeight + rightHeight); // Return height of this node to parent return Math.max(leftHeight, rightHeight) + 1; } // Maximum Path Sum variant (LeetCode 124) private int maxPathSum = Integer.MIN_VALUE; public int maxPathSum(TreeNode root) { maxGain(root); return maxPathSum; } private int maxGain(TreeNode node) { if (node == null) return 0; int leftGain = Math.max(0, maxGain(node.left)); // ignore negative gains int rightGain = Math.max(0, maxGain(node.right)); int currentMax = node.val + leftGain + rightGain; maxPathSum = Math.max(maxPathSum, currentMax); // Return the maximum sum if you go down one path from this node return node.val + Math.max(leftGain, rightGain); } }
- The return value is what the parent needs (height, maxGain).
- The global variable holds the problem's answer (diameter, maxPathSum).
- The combination step (left+right) updates the global answer.
- The return step selects the best single path to continue upward.
Pattern 5: Pre-Order Serialization & Construction (Tree Building)
Certain tree problems ask you to serialize a tree into a string (or array) and deserialize it back. Examples: 'Serialize and Deserialize Binary Tree', 'Construct Binary Tree from Preorder and Inorder Traversal'. The pattern uses pre-order traversal to create a representation, and then rebuilds the tree from that representation using a queue or index.
For serialization, you traverse pre-order, represent null children with a sentinel (e.g., 'null'), and separate values by a delimiter. The key insight: the serialized string uniquely encodes the tree structure, and the same traversal order (usually pre-order) is used to reconstruct it.
For construction from two traversals (e.g., preorder + inorder), you need both to uniquely identify the tree. Preorder gives you the root (first element), and inorder tells you which values are in left vs right subtree. A recursive approach picks the root from preorder, finds its index in inorder, then recurses on left and right slices.
package io.thecodeforge.trees; import java.util.*; public class TreeSerializer { private static final String NULL_MARKER = "null"; private static final String DELIMITER = ","; // Serialize using pre-order traversal public String serialize(TreeNode root) { StringBuilder sb = new StringBuilder(); serializeHelper(root, sb); return sb.toString(); } private void serializeHelper(TreeNode node, StringBuilder sb) { if (node == null) { sb.append(NULL_MARKER).append(DELIMITER); return; } sb.append(node.val).append(DELIMITER); serializeHelper(node.left, sb); serializeHelper(node.right, sb); } // Deserialize using a queue from the serialized array public TreeNode deserialize(String data) { String[] parts = data.split(DELIMITER); Queue<String> queue = new LinkedList<>(Arrays.asList(parts)); return deserializeHelper(queue); } private TreeNode deserializeHelper(Queue<String> queue) { String val = queue.poll(); if (val == null || val.equals(NULL_MARKER)) return null; TreeNode node = new TreeNode(Integer.parseInt(val)); node.left = deserializeHelper(queue); node.right = deserializeHelper(queue); return node; } }
Pattern 6: In-Order Traversal for BST Validation & Sorted Output
In-order traversal of a Binary Search Tree yields nodes in ascending order. This property is the foundation of two big problems: 'Validate Binary Search Tree' and 'Kth Smallest Element in a BST'. The pattern: traverse in-order, track the last visited value, and compare.
For BST validation, you need to ensure that the current node's value is greater than the previous node's value (strictly). A recursive approach passes a valid range (min, max) down the tree. The iterative in-order traversal using a stack is also popular because it avoids recursion overhead and can early exit when a violation is found.
For Kth Smallest, you can stop the traversal after processing k nodes. The iterative stack approach shines here: pop k times and return the kth popped value.
package io.thecodeforge.trees; import java.util.*; public class BSTValidator { // Recursive range-based validation public boolean isValidBST(TreeNode root) { return validate(root, null, null); } private boolean validate(TreeNode node, Integer low, Integer high) { if (node == null) return true; if ((low != null && node.val <= low) || (high != null && node.val >= high)) return false; return validate(node.left, low, node.val) && validate(node.right, node.val, high); } // Iterative in-order validation (uses stack) – early exit public boolean isValidBSTIterative(TreeNode root) { Stack<TreeNode> stack = new Stack<>(); TreeNode curr = root; Integer prev = null; while (curr != null || !stack.isEmpty()) { while (curr != null) { stack.push(curr); curr = curr.left; } curr = stack.pop(); if (prev != null && curr.val <= prev) return false; prev = curr.val; curr = curr.right; } return true; } // Kth Smallest Element in BST public int kthSmallest(TreeNode root, int k) { Stack<TreeNode> stack = new Stack<>(); TreeNode curr = root; int count = 0; while (curr != null || !stack.isEmpty()) { while (curr != null) { stack.push(curr); curr = curr.left; } curr = stack.pop(); count++; if (count == k) return curr.val; curr = curr.right; } return -1; } }
The Tree Problem Categorization Engine: Easy, Medium, Hard — and Why the Labels Lie
Every top competitor site dumps a list of 'Top 50 Tree Problems' with bullshit difficulty labels. Easy? Medium? Hard? Those aren't your interview metrics. What matters is the data structure transformation pattern you're executing.
- Easy: Single-pass traversal with constant extra state. Max depth of a binary tree. Symmetric tree check. You're reading the tree, not mutating it.
- Intermediate: You need multiple passes or auxiliary data structures. Level-order zigzag requires a double-ended queue. Lowest common ancestor builds a parent map or uses recursive backtracking.
- Advanced: The problem demands tree reconstruction or property propagation. Serialize/deserialize is advanced because you're inventing a wire format. Morris traversal for O(1) space? Advanced, and most seniors still can't write it cold.
- Expert: Self-balancing tree insertions, b-tree splits, or in-place operations that break structural invariants. These are systems-level problems.
The trap: ignoring traversal pattern and fixating on leetcode difficulty. A 'medium' problem that requires BFS on a 50k-node tree is harder than a 'hard' problem that uses simple DFS on a balanced BST. Match the pattern to the constraint, not the label.
// io.thecodeforge — interview tutorial def classify_tree_problem(problem): """Determines pattern category, not leetcode difficulty.""" patterns = { 'single_pass_constant': ['max_depth', 'is_symmetric', 'count_nodes'], 'multi_pass_auxiliary': ['zigzag_traversal', 'lowest_common_ancestor', 'right_side_view'], 'reconstruction_propagation': ['serialize', 'deserialize', 'clone_tree', 'diameter'], 'structure_breaking': ['rotate_bst', 'flatten_to_list', 'rebalance_avl'] } for category, samples in patterns.items(): if any(p in problem.lower() for p in samples): return category return 'manual_review_needed' # Examples print(classify_tree_problem('max_depth_of_binary_tree')) # single_pass_constant print(classify_tree_problem('lowest_common_ancestor')) # multi_pass_auxiliary print(classify_tree_problem('serialize_and_deserialize')) # reconstruction_propagation print(classify_tree_problem('flatten_binary_tree')) # structure_breaking print(classify_tree_problem('mystery_problem')) # manual_review_needed
Tree Reconstruction: The Forgotten Interview Pattern (and Why You Need It)
Competitor lists bury reconstruction problems under 'Advanced' but they don't explain why they matter. Here's the truth: every distributed system engineer writes tree serialization. Your config parser outputs a JSON tree. Your gRPC service serializes a protobuf tree. Building a tree from flattened data is not academic — it's Tuesday.
The canonical problem: 'Construct Binary Tree from Preorder and Inorder Traversal'. Most devs memorize the recursive partition hack. They don't understand why it works. The why: preorder gives you root positions. Inorder defines left/right boundaries. You explode the domain recursively.
But here's the production-grade variant: 'Serialize a tree to a compact format, then deserialize it without duplicates or ambiguity.' That's the bread-and-butter of database index deserialization. The trick? Use sentinels for nulls (like Leetcode's '#' for null nodes) and enforce post-order or pre-order ordering so you rebuild without extra structure.
The pattern scales: tree → flattened string → tree. Any distributed cache system does this. Your job is to pick a format that's human-debuggable and computationally cheap. Pre-order with null sentinels wins. In-order serialization is ambiguous without the entire tree shape. Don't be that engineer.
// io.thecodeforge — interview tutorial class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class TreeSerializer: NULL_SENTINEL = '#' def serialize(self, root): """Pre-order with sentinel for nulls. O(n) time, O(h) space.""" if not root: return self.NULL_SENTINEL left = self.serialize(root.left) right = self.serialize(root.right) return f"{root.val},{left},{right}" def deserialize(self, data): """Rebuild from pre-order sentinel format.""" values = data.split(',') self._index = 0 return self._build(values) def _build(self, values): if self._index >= len(values): return None val = values[self._index] self._index += 1 if val == self.NULL_SENTINEL: return None node = TreeNode(int(val)) node.left = self._build(values) node.right = self._build(values) return node # Usage serializer = TreeSerializer() root = TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4))) packed = serializer.serialize(root) print(packed) # "1,2,#,#,3,4,#,#,#" restored = serializer.deserialize(packed) print(restored.val) # 1
The Moron's Guide to Tree Space Complexity: Stack vs. Heap vs. Call Stack
Here's what every 'Top 50 Problems' list omits: the space complexity of your traversal function is not just the algorithm — it's your runtime environment. Recursive DFS costs O(h) on the call stack. That 'Easy' problem on Leetcode is O(log n) for a balanced tree, but O(n) for a degenerate tree. Guess which one shows up in production data?
Competitors say 'Iterative DFS with explicit stack is better for large trees.' They're right but for the wrong reasons. The real reason: recursion depth is bounded by your thread stack (typically 1MB on Linux). At ~8 bytes per stack frame, you blow past 1MB at ~131k recursive calls. A 50k-node degenerate tree? You're fine. A 200k-node array-as-tree? Segfault.
The fix: iterative traversal with an explicit stack on the heap. The heap can grow to gigabytes. Your call stack cannot. Every senior engineer knows this. The coders who fail the on-site don't.
BFS queue space? That's O(w) where w is max width. For a complete binary tree of n nodes, w ≈ n/2. That's O(n) heap memory — but it won't blow the stack. Trade-offs exist. Understand them before the interviewer asks about memory constraints on a 10k-node tree.
// io.thecodeforge — interview tutorial import sys class TreeNode: def __init__(self, val, left=None, right=None): self.val = val self.left = left self.right = right def recursive_dfs(node, depth=0): """Watch the stack grow. Don't run this on a 100k-node chain.""" if not node: return if depth > 1000: print(f"Crossed {depth} frames — danger zone") recursive_dfs(node.left, depth + 1) recursive_dfs(node.right, depth + 1) def iterative_dfs(root): """Heap stack — safe for any tree depth.""" stack = [(root, 0)] while stack: node, depth = stack.pop() if not node: continue if depth > 1000000: print("1M depth — heap has room") stack.append((node.right, depth + 1)) stack.append((node.left, depth + 1)) # Build a degenerate chain of 10000 nodes nodes = [TreeNode(i) for i in range(10000)] for i in range(len(nodes) - 1): nodes[i].left = nodes[i + 1] print(f"Default recursion limit: {sys.getrecursionlimit()}") try: recursive_dfs(nodes[0]) # Will hit recursion limit except RecursionError as e: print(f"Recursion failed: {e}") print("Iterative: safe for huge depths") iterative_dfs(nodes[0]) print(f"Call stack depth: ~{sys.getrecursionlimit()} vs heap space: unlimited")
Recursive Stack Overflow on 50,000-Node Tree
- Never rely on recursion for tree traversal unless you control the tree shape or you've added a depth guard.
- Always ask: could the tree be degenerate? If yes, use iterative traversal.
- For production code, prefer iterative BFS or DFS with explicit stack/queue over recursion.
Add a static AtomicInteger recursionDepth = new AtomicInteger(0);Check with `jstack <pid>` to see the thread stack depth.Run with -ea (enable assertions) and add `assert node != null : "Node is null at depth " + depth;`Log the current node value and its children before access.Add a debug print: `System.out.println("Visiting: " + node.val);`Trace by hand on a small sample tree (3–5 nodes).| Algorithm | Mechanism | Best Used For | Time Complexity | Space Complexity |
|---|---|---|---|---|
| DFS (Pre-order) | Node -> Left -> Right | Serializing/Cloning trees | O(N) | O(H) — H = height (stack) |
| DFS (In-order) | Left -> Node -> Right | Validating BSTs (Returns sorted values) | O(N) | O(H) |
| DFS (Post-order) | Left -> Right -> Node | Deleting trees, Diameter, Depth | O(N) | O(H) |
| BFS (Level-order) | Queue-based Layering | Level order, Shortest path, Right-side view | O(N) | O(W) — W = max tree width |
| Two-Pointer (Lockstep) | Recursive comparison | Same Tree, Symmetric Tree | O(N) | O(H) |
| Serialization (Pre-order + nulls) | Pre-order traversal with sentinel | Tree serialization/deserialization | O(N) | O(N) — output string size |
| File | Command / Code | Purpose |
|---|---|---|
| io | /** | Pattern 1 |
| io | public class LevelExplorer { | Pattern 2 |
| io | public class TreeComparison { | Pattern 3 |
| io | public class BottomUpAggregator { | Pattern 4 |
| io | public class TreeSerializer { | Pattern 5 |
| io | public class BSTValidator { | Pattern 6 |
| CategorizeByPattern.py | def classify_tree_problem(problem): | The Tree Problem Categorization Engine: Easy, Medium, Hard |
| TreeSerializer.py | class TreeNode: | Tree Reconstruction |
| SpaceComparison.py | class TreeNode: | The Moron's Guide to Tree Space Complexity |
Key takeaways
Common mistakes to avoid
5 patternsNot handling the null root case
Confusing Binary Trees with Binary Search Trees (BST)
Infinite recursion or stack overflow due to missing base case
Misordering null checks in structural comparison (same tree)
Assuming BFS always uses less memory than DFS
Interview Questions on This Topic
Write a function to check if a binary tree is symmetric around its center.
java
public boolean isSymmetric(TreeNode root) {
return root == null || isMirror(root.left, root.right);
}
private 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);
}
``
Complexity: O(N) time, O(H) space (recursion stack).Find the diameter of a binary tree. The diameter is the length of the longest path between any two nodes, measured by the number of edges.
diameter with Math.max(diameter, leftHeight + rightHeight). The return value to the parent is Math.max(leftHeight, rightHeight) + 1.
Key insight: the longest path through a node equals the sum of its left and right subtree heights. Track the maximum over all nodes.
``java
private int diameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
height(root);
return diameter;
}
private int height(TreeNode node) {
if (node == null) return 0;
int left = height(node.left);
int right = height(node.right);
diameter = Math.max(diameter, left + right);
return Math.max(left, right) + 1;
}
``Serialize and deserialize a binary tree. The serialized string should be compact and the deserialized tree identical to the original.
java
private static final String NULL = "null";
private static final String SEP = ",";
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
serializeHelper(root, sb);
return sb.toString();
}
private void serializeHelper(TreeNode node, StringBuilder sb) {
if (node == null) { sb.append(NULL).append(SEP); return; }
sb.append(node.val).append(SEP);
serializeHelper(node.left, sb);
serializeHelper(node.right, sb);
}
public TreeNode deserialize(String data) {
Queue<String> queue = new LinkedList<>(Arrays.asList(data.split(SEP)));
return deserializeHelper(queue);
}
private TreeNode deserializeHelper(Queue<String> queue) {
String val = queue.poll();
if (val == null || val.equals(NULL)) return null;
TreeNode node = new TreeNode(Integer.parseInt(val));
node.left = deserializeHelper(queue);
node.right = deserializeHelper(queue);
return node;
}
``
Time: O(N), Space: O(N) for the serialized string.Given two binary trees, check if one tree is a subtree of the other.
isSameTree function. If T2 is null, it's a subtree of any non-null T1. If T1 is null, only null T2 can be a subtree.
Optimization: use a single recursion that combines the checking – for each node in T1, check if isSameTree(node, t2). If true, return true; else recursively check left and right children.
``java
public boolean isSubtree(TreeNode s, TreeNode t) {
if (t == null) return true;
if (s == null) return false;
if (isSameTree(s, t)) return true;
return isSubtree(s.left, t) || isSubtree(s.right, t);
}
private boolean isSameTree(TreeNode s, TreeNode t) {
if (s == null && t == null) return true;
if (s == null || t == null) return false;
if (s.val != t.val) return false;
return isSameTree(s.left, t.left) && isSameTree(s.right, t.right);
}
``
Time: O(m * n) in worst case (m nodes in main, n in subtree). Can be improved to O(m + n) using serialization and KMP.Frequently Asked Questions
A Binary Tree is any tree where each node has at most two children. A Binary Search Tree (BST) is a specific type of binary tree where for every node, the left child's value is less than the parent, and the right child's value is greater. This allows for $O(\log N)$ search time.
Trees are recursive by definition — every child of a node is itself the root of a smaller tree. Recursive solutions are cleaner and mirror the data structure's inherent logic, though iterative solutions using explicit stacks or queues are safer for extremely deep trees to avoid stack overflow.
A binary tree is considered balanced if the heights of the left and right subtrees of every node differ by no more than one. Balancing is crucial (e.g., AVL trees or Red-Black trees) to maintain $O(\log N)$ performance and prevent the tree from degrading into a linked list.
Use iterative DFS when the tree might be deep (e.g., degenerate tree with 10000+ nodes). Recursive DFS uses the call stack which is limited; iterative uses a heap-allocated stack. Also use iterative when you need to stop early (e.g., Kth smallest) or when recursion overhead is a concern.
For very wide trees, BFS queue can become large (up to N/2). If memory is a concern, consider using an iterative DFS solution instead. Alternatively, you can process BFS in chunks (e.g., using external storage) but that's complex. Know your tree shape before choosing BFS.
20+ years shipping production code across the stack, with years spent interviewing engineers. Lessons pulled from things that broke in production.
That's Coding Patterns. Mark it forged?
6 min read · try the examples if you haven't