Tree DFS Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide
Solve a tree by asking each child for an answer (recursion), then combine. Decide what each call returns and what it receives.
6 sub-patterns Β· 26 questions
What is this pattern?
Tree DFS goes as deep as possible down one branch before coming back, usually with recursion. Almost every tree question is solved by writing one small function and trusting it on the two subtrees: "if f already works for my left child and my right child, how do I get my own answer?"
The whole pattern is deciding which way the information flows:
- Top-down: the parent passes something down as a parameter (the sum so far, the max on the path, the allowed range in a BST). Leaves check it.
- Bottom-up: each child returns a summary of its subtree (height, best path, "did I find p?"). The parent combines the two, and a global best is updated along the way.
Every node is visited once, so it is O(n) time. The recursion stack holds one root-to-node path: O(h) space, where h is the height (log n when balanced, n when the tree is a chain).
A manager asking a team for a report. Top-down: the manager hands each team lead a budget ("you have 12 left"), and each lead passes what's left to their own team. Bottom-up: every team lead first collects their team's totals and sends one number up. The manager never looks at individual workers, only at the two numbers from the two leads.
See it move
Blue = the current path, red = finished (left the path), purple = a leaf whose path hits the target. The note under a node is the sum from the root down to it.
root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22step 1/15- target
- 22
Each call receives the sum of the path above it. The state flows from the root down, and each node only adds its own value.
Blue = combining now, green = finished (note = height it returned), purple = where the best path bends.
root = [1,2,null,4,5,8,null,null,6,null,null,null,7]step 1/9- best
- 0
A node can't know its height until both children have told it theirs, so the work happens on the way back up (post-order). The note under each finished node is the height it returned.
How do I recognise it?
"Maximum depth", "height", "diameter", "balanced": answers built from both subtrees.
"Root-to-leaf path" with a sum, digits, a max so far, or a direction: carry it down.
"Any path" / "maximum path sum": each node returns its best one-sided path; the best two-sided path is a global.
A binary search tree: left < node < right. Search one side; inorder is sorted; pass a valid range down.
"Lowest common ancestor": the first node where the two targets end up on different sides.
Compare, invert, flatten, build, serialize a tree: a recursive walk that handles one node and trusts the recursion for the rest.
Looks like Tree DFS, but isn't
The answer is grouped by depth, and BFS visits depth by depth. Minimum depth also lets BFS stop at the first leaf instead of exploring every path.
A path that starts anywhere on the way down is a subarray of the current root-to-node path. A prefix-sum hash map counts them in O(n), instead of starting a DFS from every node.
It is still bottom-up DFS, but each node returns two answers (take it / skip it), like a DP state, so no node is solved twice.
A trie is a tree you build from the words. The DFS happens on it, but the idea is sharing common prefixes.
How would I discover it from scratch?
Don't memorise βuse tree dfsβ. Ask these questions in order β the pattern falls out.
Pretend the recursion already works for both children. What do you need from them, or what do they need from you? "My height = 1 + max of theirs" is bottom-up. "My remaining sum = what my parent had β my value" is top-down.
Sub-patterns
Same core idea, different rule for βvalidβ. Learn them in this order.
Traversal / comparison
5 questionsVisit every node in a fixed order, or walk two trees side by side and compare them node by node.
Path state (top-down)
6 questionsCarry information down a root-to-leaf path as a parameter, and check or record it when you reach a leaf.
Subtree answers (bottom-up)
4 questionsEach node returns a small summary of its subtree, and the global answer is updated while combining the two children.
BST properties
4 questionsUse the BST order: left < node < right. Search one side, read values in sorted order with inorder, or pass a valid range down.
Lowest common ancestor
3 questionsFind the deepest node that has both targets in its subtree: the first node where the two targets split to different sides.
Build / transform / serialize
4 questionsChange the tree's shape, or rebuild a tree from a description of it. Choose the visiting order so you never lose a pointer you still need.
Don't mix these up
Does a node need something from above (the sum so far, the max on the path, a BST range)? Pass it down. Does it need something from below (heights, best path, found or not)? Return it up. Some questions use both: Count Good Nodes passes the max down and returns the count up.
When the best path can bend at any node, a node returns only what its parent can extend (one side). The two-sided value is compared with a global at every node. Returning both sides upward would build a path that forks.
A root-to-leaf path ends at a node with no children. Checking at null treats the empty side of a one-child node as a path end: Path Sum gets a false hit, and Minimum Depth (the min version) returns 1 on a chain.
[5,4,6,null,null,3,7] passes every parent-child check, but 3 sits in 5's right subtree. Each node must be inside the range from every ancestor, which the (low, high) parameters carry.
The value rule only works because a BST is ordered. On a plain binary tree the values say nothing about where p and q are, so you need the post-order "what did each side find" search.
Code templates (C++)
Traversals and walking two trees
Pre-, in- and post-order differ only in where you handle the node. The iterative inorder goes left as far as it can, pops, visits, then goes right. Two trees: compare the pair, then recurse on matching children.
// Recursive DFS: the three orders differ only in WHERE you handle the node.
void dfs(TreeNode* node, vector<int>& out) {
if (!node) return;
// preorder: out.push_back(node->val); (node before its children)
dfs(node->left, out);
out.push_back(node->val); // inorder: between the two children
dfs(node->right, out);
// postorder: out.push_back(node->val); (node after both children)
}
// Iterative inorder with your own stack: go left as far as possible, then visit, then go right.
vector<int> inorderIterative(TreeNode* root) {
vector<int> out;
stack<TreeNode*> st;
TreeNode* cur = root;
while (cur || !st.empty()) {
while (cur) { st.push(cur); cur = cur->left; } // everything on the way down waits its turn
cur = st.top(); st.pop();
out.push_back(cur->val); // its whole left side is already done
cur = cur->right;
}
return out;
}
// Two trees at once: compare the pair, then recurse on matching pairs of children.
bool isSameTree(TreeNode* p, TreeNode* q) {
if (!p || !q) return p == q; // both null β same; one null β different
return p->val == q->val && isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}Top-down: carry the path state
Pass the state as a parameter; check at a leaf. A shared path vector is pushed on entry and popped on exit, so siblings never see each other's nodes.
// Top-down: carry the path's state as a parameter; check it at the leaves.
// Here: every root-to-leaf path whose sum is target (Path Sum II).
void walk(TreeNode* node, int remaining, vector<int>& path, vector<vector<int>>& out) {
if (!node) return;
path.push_back(node->val); // enter: add this node to the path
remaining -= node->val;
if (!node->left && !node->right) { // a leaf: the path is complete
if (remaining == 0) out.push_back(path);
} else {
walk(node->left, remaining, path, out);
walk(node->right, remaining, path, out);
}
path.pop_back(); // leave: undo, so the parent's path is clean
}
vector<vector<int>> pathSum(TreeNode* root, int target) {
vector<vector<int>> out;
vector<int> path;
walk(root, target, path, out);
return out;
}Bottom-up: return a summary, update a global
Each call returns what its parent can extend (one side only). The best answer bends at some node and uses both sides, so it is kept in a separate global.
// Bottom-up: each call returns a summary of its subtree; the global answer is
// updated while combining the two children. Here: diameter (longest path, in edges).
int best = 0;
int height(TreeNode* node) { // returns: nodes on the longest downward path
if (!node) return 0;
int lh = height(node->left);
int rh = height(node->right);
best = max(best, lh + rh); // the longest path that bends at this node
return 1 + max(lh, rh); // a parent can only extend ONE side
}
int diameterOfBinaryTree(TreeNode* root) {
best = 0;
height(root);
return best;
}BST: range check and one-sided search
Every node must sit inside (low, high) from its ancestors. Use long long bounds so values equal to INT_MIN / INT_MAX still pass.
// BST: every node must fit inside the (low, high) range set by its ancestors.
// long long bounds so a node equal to INT_MIN or INT_MAX still fits.
bool valid(TreeNode* node, long long low, long long high) {
if (!node) return true;
if (node->val <= low || node->val >= high) return false;
return valid(node->left, low, node->val) // left side: everything below node->val
&& valid(node->right, node->val, high); // right side: everything above node->val
}
bool isValidBST(TreeNode* root) { return valid(root, LLONG_MIN, LLONG_MAX); }
// Searching only needs one side: the comparison tells you where the value must be.
TreeNode* searchBST(TreeNode* root, int target) {
while (root && root->val != target)
root = target < root->val ? root->left : root->right;
return root;
}Lowest common ancestor in one pass
Return the node itself if it is p or q; if both sides return something, this node is where they split. In a BST, compare values instead: both smaller β left, both larger β right.
// Lowest common ancestor in a plain binary tree, in one post-order pass.
// Each call returns p or q if it found one below (or is one), else nullptr.
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left && right) return root; // p and q split here: this is the LCA
return left ? left : right; // pass up whichever side found something
}Build from preorder + inorder
Preorder gives the next root; its index in inorder (from a hash map) splits the two subtrees. Build the left subtree first, because preorder lists root, left, right.
// Build from preorder + inorder: preorder gives the next root, its position
// in inorder splits the left and right subtrees.
TreeNode* build(vector<int>& pre, int& next, int lo, int hi, unordered_map<int, int>& pos) {
if (lo > hi) return nullptr; // empty inorder range β no subtree
int val = pre[next++];
TreeNode* node = new TreeNode(val);
int mid = pos[val];
node->left = build(pre, next, lo, mid - 1, pos); // left first: preorder is root, left, right
node->right = build(pre, next, mid + 1, hi, pos);
return node;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
unordered_map<int, int> pos; // value β index in inorder (values are unique)
for (int i = 0; i < (int)inorder.size(); i++) pos[inorder[i]] = i;
int next = 0;
return build(preorder, next, 0, (int)inorder.size() - 1, pos);
}