Tree DFS
Solve tree problems by recursing into subtrees, either pushing context down (top-down) or bubbling results up (bottom-up), so one small function handles every node the same way.
Instinct If the answer for a node can be built from the answers of its left and right subtrees, write one recursive function and trust it on the children.
Memory hack Ask the kids, then decide - each child reports up, the parent combines, and a global variable keeps the best "bend" seen so far.
At a glance
When to use it
- The answer for a node is built from the answers of its left and right subtrees
- You need heights, diameters, balance checks or subtree sums
- You track root-to-leaf paths or a running value passed down from ancestors
- Two trees are compared node by node, or a tree is built, inverted or flattened
Key idea
Write one recursive function with a null base case that either passes context down or returns a summary up; each node is processed once for O(n) instead of recomputing subtree facts again and again, and a global keeps answers that bend through a node.
Typical complexity
O(n) time, O(h) space for the recursion stack
| # | Common problems | Level | Our solution | Practice |
|---|---|---|---|---|
| 1 | Maximum Depth of Binary Tree | Easy | Read solution | LeetCode ↗ |
| 2 | Invert Binary Tree | Easy | Read solution | LeetCode ↗ |
| 3 | Same Tree | Easy | Read solution | LeetCode ↗ |
| 4 | Diameter of Binary Tree | Easy | Read solution | LeetCode ↗ |
| 5 | Lowest Common Ancestor of a Binary Tree | Medium | Read solution | LeetCode ↗ |
| 6 | Binary Tree Maximum Path Sum | Hard | Read solution | LeetCode ↗ |
The instinct
A binary tree is a node plus two smaller binary trees. That self-similarity means most tree questions can be phrased as: "if I already knew the answer for my left subtree and my right subtree, how do I produce mine?" Write that rule once, add a base case for null, and the recursion does the walking. The only real design decision is which direction information flows:
- Top-down (pre-order): the parent passes something down - the running sum, the current path, the min/max allowed, the depth.
- Bottom-up (post-order): each child returns a summary - height, subtree sum, "found target?", "is balanced?" - and the parent combines them.
Recognise it
| Signal in the problem | Why it points here |
|---|---|
| "Height", "depth", "diameter", "balanced" | Each is a function of the children's heights - bottom-up |
| "Path from root to leaf with sum...", "all root-to-leaf paths" | Carry the path/sum down; act at leaves - top-down with backtracking |
| "Any path" / "path may start and end anywhere" (max path sum, Path Sum III) | Combine at the turning node; return only one branch upward |
| "Same / symmetric / subtree of another tree" | Recurse on two trees in lock-step |
| "Lowest common ancestor" | Each subtree reports whether it found p or q; first node with both is the answer |
| "Build / serialize / flatten / invert the tree" | Structural recursion: solve children, then wire the node |
| "Max difference between node and ancestor", "count good nodes" | Pass ancestor min/max down |
Not this pattern if...
- The question is about levels or "first row where..." - Tree BFS reads more naturally.
- The tree is a BST and you need order statistics or a search - exploit ordering (BST pattern) instead of visiting everything.
- The structure is a general graph with cycles - you need a visited set (graph DFS).
Memory hack
"Ask the kids, then decide." Every node asks its left and right child for their report, makes a decision, then sends a single report up to its own parent. If the best answer could pass through a node (bend at it), record that in a global - but only ever return a straight line upward.
Analogy - a company org chart. The CEO wants total headcount. She does not count everyone; she asks each direct report for their team's count, adds them, adds herself. Each manager does the same. Interns (leaves) answer "1", and empty desks (null) answer "0".
Say it out loud in the interview:
- "Base case: what do I return for
null(and maybe for a leaf)?" - "What does each child need to tell me - or what do I need to tell each child?"
- "How do I combine at this node, and is there a separate global answer for paths that bend here?"
How it works
Bottom-up for Diameter of Binary Tree: height(node) = 1 + max(height(left), height(right)), while the diameter candidate at that node is height(left) + height(right) (edges through the node). The function returns height but updates the diameter.
flowchart TD
A["dfs(node)"] --> B{"node == null?"}
B -->|yes| C["return neutral value (0, true, null...)"]
B -->|no| D["L = dfs(left)"]
D --> E["R = dfs(right)"]
E --> F["update global with L + node + R (the bend)"]
F --> G["return node + best single branch to parent"]
Trace on 1 -> (2, 3), 2 -> (4, 5):
| Node | L height | R height | Bend = L + R | Returns |
|---|---|---|---|---|
| 4 | 0 | 0 | 0 | 1 |
| 5 | 0 | 0 | 0 | 1 |
| 2 | 1 | 1 | 2 | 2 |
| 3 | 0 | 0 | 0 | 1 |
| 1 | 2 | 1 | 3 | 3 |
Java template
import java.util.*; class TreeNode { int val; TreeNode left, right; TreeNode(int v) { val = v; } } class TreeDfs { // BOTTOM-UP: return a summary upward, keep the best "bend" in a field. private int best = Integer.MIN_VALUE; int solve(TreeNode root) { best = Integer.MIN_VALUE; up(root); return best; } private int up(TreeNode node) { if (node == null) return 0; // neutral value for an empty subtree int l = Math.max(0, up(node.left)); // drop negative branches (max path sum) int r = Math.max(0, up(node.right)); best = Math.max(best, l + node.val + r); // answer that bends at this node return node.val + Math.max(l, r); // only ONE branch may continue upward } // TOP-DOWN with backtracking: carry state to children, act at leaves. static void down(TreeNode node, int sumSoFar, Deque<Integer> path, List<List<Integer>> out, int target) { if (node == null) return; sumSoFar += node.val; path.addLast(node.val); // choose if (node.left == null && node.right == null && sumSoFar == target) { out.add(new ArrayList<>(path)); // copy - path keeps changing } down(node.left, sumSoFar, path, out, target); down(node.right, sumSoFar, path, out, target); path.removeLast(); // un-choose } // TWO-TREE lock-step (same tree / symmetric / subtree check). static boolean same(TreeNode a, TreeNode b) { if (a == null || b == null) return a == b; return a.val == b.val && same(a.left, b.left) && same(a.right, b.right); } }
Complexity
- Time: O(n) - each node is visited a constant number of times. Watch out for hidden O(n) work per node (e.g. recomputing height inside a balance check, or
indexOfin construction), which makes it O(n^2); precompute or return more from the recursion. - Space: O(h) recursion stack - O(log n) for balanced trees, O(n) for a skewed one. Very deep inputs can overflow the Java stack; mention an iterative version with an explicit stack.
Variations
- Bottom-up aggregate - height, size, sums. Maximum Depth of Binary Tree, Diameter of Binary Tree, Distribute Coins in Binary Tree.
- Global "bend" answer - return one branch, record both. Binary Tree Maximum Path Sum.
- Top-down state - pass bounds, sums or ancestors' min/max. Maximum Difference Between Node and Ancestor; Path Sum III adds a prefix-sum hash map that is backtracked on the way out.
- Search with reports - children return the node they found. Lowest Common Ancestor of a Binary Tree.
- Two trees together - Same Tree, Symmetric Tree, Subtree of Another Tree.
- Build / reshape - Construct Binary Tree from Preorder and Inorder Traversal (map value to inorder index), Invert Binary Tree, Flatten Binary Tree to Linked List, Delete Nodes And Return Forest.
- Encode subtrees - serialise each subtree to a string or id to spot repeats. Find Duplicate Subtrees, Serialize and Deserialize Binary Tree (pre-order with null markers).
Common mistakes
Returning the "bent" value (left + node + right) to the parent. A path cannot fork, so the parent may only extend one branch; the bend goes into the global answer, never into the return value.
- Treating a node with one child as a leaf (minimum depth, root-to-leaf sums). A leaf has both children null.
- Forgetting to undo state after recursing (path lists, prefix-sum counts), so siblings see their cousin's data.
- Adding the mutable
pathitself to the result instead of a copy - every stored path ends up empty. - Initialising a global max to
0when all node values may be negative; start atInteger.MIN_VALUE, or seed with the root. - Using a
staticfield without resetting it between test cases.
All practice problems
22 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.
Easy 11
Medium 9
- Path Sum III
- Maximum Difference Between Node and Ancestor
- Construct Binary Tree from Preorder and Inorder Traversal
- Construct Binary Tree from Inorder and Postorder Traversal
- Delete Nodes And Return Forest
- Lowest Common Ancestor of a Binary Tree
- Find Duplicate Subtrees
- Flatten Binary Tree to Linked List
- Distribute Coins in Binary Tree