Tree Depth-First Search
Solve a tree problem by solving it for the left and right subtrees and combining the answers on the way back up.
- O(n)Time: each node is entered once
- O(h)Stack: log n balanced, n skewed
- ~10,000 framesRough V8 limit; a 10^5 chain overflows
Show what to say
“For a tree problem I assume the recursive call works on each subtree, then write only the base case and the combine step at the node. Answers return up or are carried down, and paths that bend keep a global best. That is O(n) time and O(h) stack.”
Trade-offs
- Recursive vs iterative DFS: recursion is shorter and easier to get right; an explicit stack has no depth limit but is more code.
- Return heights vs recompute them: one post-order pass is O(n); recomputing height at every node is O(n²) on a skewed tree.
- DFS vs BFS on a tree: DFS holds one path, BFS holds one level and can stop at the first leaf; BFS suits level questions.
Bring it up before they ask
A leaf has both children null (a one-child node is not a leaf), start maximums at -Infinity when values can be negative, and copy a shared path array before storing it.
A tree is a root plus two smaller trees, so solve both subtrees, then combine at the node. Answers return up or are carried down; paths that bend keep a global best. O(n) time, O(h) stack.
When to reach for it
Tap the signals you can see in the problem.
Rememberbinary tree or BSTRoot-to-leaf pathslongest path anywhere
- Return a value up
Information flowschildren → parent
The node workspost-order, after both children
- Carry state down
Information flowsparent → children, as parameters
The node workspre-order, before the children
- Global best
Information flowsreturn one value, record another
The node workspost-order, plus an outer variable
- BST walk
Information flowscompare, then one branch only
The node workstop-down
- Explicit stack
Information flowsan array replaces the call stack
The node worksany order
The core idea: trust the recursive call
Picture a manager with two teams.
Read more · 165 words
Ask each team lead for their team's answer, then add your own part. You never ask how the leads got theirs. That is trusting the recursive call: assume f(node.left) and f(node.right) already work. You write only:
- the base case: the answer for an empty tree;
- the combine step: this node's answer, from the two below.
Each node is entered once (O(n)); one call waits per level (O(h) stack).
The three depth-first orders differ in when the node works. For the BST [8, 3, 10, 1, 6, null, 14]:
| Order | Work happens | Visits | Use it to |
|---|---|---|---|
| Pre-order | before the children | 8, 3, 1, 6, 10, 14 | pass depth, paths or bounds down; copy a tree |
| In-order | between left and right | 1, 3, 6, 8, 10, 14 | read a BST in sorted order |
| Post-order | after both children | 1, 6, 3, 14, 10, 8 | combine children's answers; delete a tree |
In post-order, each node returns its height (the badge) to its parent:
// The three depth-first orders differ only in WHEN a node does its work relative to its children.
export function traversals(root) {
const pre = [];
const inorder = [];
const post = [];
const dfs = (node) => {
if (node === null) return;
pre.push(node.val); // before the children: top-down work (pass something down)
dfs(node.left);
inorder.push(node.val); // between them: on a BST this visits values in sorted order
dfs(node.right);
post.push(node.val); // after both: bottom-up work (combine the children's answers)
};
dfs(root);
return { pre, inorder, post };
}Return a value up: the height of every subtree
- node
- "8"
Call height(8) on the root. A node can't know its height until both subtrees have answered, so the work happens on the way back up (post-order).
function height(node) {
if (node === null) return 0;
const left = height(node.left);
const right = height(node.right);
return 1 + Math.max(left, right);
}All steps as text
- Call height(8) on the root. A node can't know its height until both subtrees have answered, so the work happens on the way back up (post-order).
- height(8) first asks its left subtree, rooted at 3, and waits on the call stack.
- height(3) first asks its left subtree, rooted at 1, and waits on the call stack.
- 1 is a leaf. Both of its children are null, and the base case says an empty subtree has height 0.
- Both answers are in: 1 + max(0, 0) = 1. height(1) returns 1 to height(3) and leaves the call stack.
- The left side answered 1. Now height(3) asks its right subtree, rooted at 6.
- height(6) first asks its left subtree, rooted at 4, and waits on the call stack.
- 4 is a leaf. Both of its children are null, and the base case says an empty subtree has height 0.
- Both answers are in: 1 + max(0, 0) = 1. height(4) returns 1 to height(6) and leaves the call stack.
- The left side answered 1. Now height(6) asks its right subtree, rooted at 7.
- 7 is a leaf. Both of its children are null, and the base case says an empty subtree has height 0.
- Both answers are in: 1 + max(0, 0) = 1. height(7) returns 1 to height(6) and leaves the call stack.
- Both answers are in: 1 + max(1, 1) = 2. height(6) returns 2 to height(3) and leaves the call stack.
- Both answers are in: 1 + max(1, 2) = 3. height(3) returns 3 to height(8) and leaves the call stack.
- The left side answered 3. Now height(8) asks its right subtree, rooted at 10.
- 10 has no left child: height(null) returns 0 straight away.
- The left side answered 0. Now height(10) asks its right subtree, rooted at 14.
- 14 is a leaf. Both of its children are null, and the base case says an empty subtree has height 0.
- Both answers are in: 1 + max(0, 0) = 1. height(14) returns 1 to height(10) and leaves the call stack.
- Both answers are in: 1 + max(0, 1) = 2. height(10) returns 2 to height(8) and leaves the call stack.
- Both answers are in: 1 + max(3, 2) = 4. height(8) returns 4 to the caller and leaves the call stack.
- Every node was visited once and the root returned 4: the longest root-to-leaf path, 8 → 3 → 6 → 4, has 4 nodes. O(n) time, O(h) stack.
Play with it
Watch the walker dive while the call stack grows and shrinks. Then pick Predict and click the nodes in order yourself.
Read more · 78 words
Say what the function returns for one subtree
One sentence, like "the height of the subtree at node".
Write the base case
if (node === null) return …: 0 for sums,-Infinityfor maximums.Decide what flows down and what flows up
Ancestors' info goes down as parameters; descendants' info comes back as return values.
Write the combine step
Build the answer from
left,rightandnode.val.Check the shapes that break trees
Empty, one node, skewed, negative values.
Templates
Template 1: return a value up
Ask both subtrees, then combine: 1 + Math.max(left, right) for height.
That's Maximum Depth of Binary TreeEasy. Invert Binary TreeEasy swaps the children in the combine. Lowest Common Ancestor of a Binary TreeMedium: the first node to hear from both sides is the answer.
// Template 1: return a value up (post-order). Ask both subtrees for their answer, then combine at the node.
// Use it for height, size, sums, "is it balanced?": anything a node can compute from its children's answers.
export function height(root) {
if (root === null) return 0; // an empty subtree has height 0, so a leaf gets 1
const left = height(root.left);
const right = height(root.right);
return 1 + Math.max(left, right); // combine on the way back up
}
// Same shape, different combine. -1 means "unbalanced somewhere below", so one number carries both answers
// and the whole check stays O(n) instead of recomputing heights at every node.
export function isBalanced(root) {
const check = (node) => {
if (node === null) return 0;
const left = check(node.left);
if (left === -1) return -1;
const right = check(node.right);
if (right === -1 || Math.abs(left - right) > 1) return -1;
return 1 + Math.max(left, right);
};
return check(root) !== -1;
}Template 2: carry state down
Use what the ancestors passed in, then call the children with an updated copy.
Read more · 30 words
A shared array must be popped after the call, and copied when stored.
Path SumEasy carries the remaining sum down and checks it at each leaf. Validate Binary Search TreeMedium carries the allowed range.
Carry state down: count the good nodes
- maxSoFar
- "-∞"
- good
- 0
Start at the root with maxSoFar = -∞: nothing is above the root, so it is good whatever its value. Each badge shows the max a node receives from its ancestors.
function goodNodes(root) {
const dfs = (node, maxSoFar) => {
if (node === null) return 0;
const good = node.val >= maxSoFar ? 1 : 0;
const nextMax = Math.max(maxSoFar, node.val);
return good + dfs(node.left, nextMax) + dfs(node.right, nextMax);
};
return dfs(root, -Infinity);
}All steps as text
- Start at the root with maxSoFar = -∞: nothing is above the root, so it is good whatever its value. Each badge shows the max a node receives from its ancestors.
- 5 ≥ -∞, the largest value above it, so 5 is good (count 1).
- Hand max(-∞, 5) = 5 down to the children. Each child gets its own copy, so nothing has to be undone afterwards.
- 3 is smaller than 5, which sits somewhere above it on the path, so it is not good.
- Hand max(5, 3) = 5 down to the children. Each child gets its own copy, so nothing has to be undone afterwards.
- 6 ≥ 5, the largest value above it, so 6 is good (count 2).
- Hand max(5, 6) = 6 down to the children. Each child gets its own copy, so nothing has to be undone afterwards.
- 7 ≥ 6, the largest value above it, so 7 is good (count 3).
- 2 is smaller than 5, which sits somewhere above it on the path, so it is not good.
- 8 ≥ 5, the largest value above it, so 8 is good (count 4).
- Hand max(5, 8) = 8 down to the children. Each child gets its own copy, so nothing has to be undone afterwards.
- 7 is smaller than 8, which sits somewhere above it on the path, so it is not good.
- 9 ≥ 8, the largest value above it, so 9 is good (count 5).
- Every call added its own 1 or 0 to what its children returned, and the root's call returns 5. Information flowed down (the max) and the count flowed back up.
// Template 2: carry state down (pre-order). Each call receives what its ancestors know (the max so far, a running
// sum, the allowed range) and hands an updated value to its children. Primitives are copied, so nothing to undo.
export function goodNodes(root) {
// A node is "good" when nothing on the path from the root down to it is larger than it.
const dfs = (node, maxSoFar) => {
if (node === null) return 0;
const good = node.val >= maxSoFar ? 1 : 0;
const nextMax = Math.max(maxSoFar, node.val);
return good + dfs(node.left, nextMax) + dfs(node.right, nextMax);
};
return dfs(root, -Infinity);
}
// Carrying a whole path down: push before recursing, pop after (backtracking), copy it only at a leaf.
export function rootToLeafPaths(root) {
const paths = [];
const path = [];
const dfs = (node) => {
if (node === null) return;
path.push(node.val);
if (node.left === null && node.right === null) paths.push([...path]); // copy: path keeps changing
dfs(node.left);
dfs(node.right);
path.pop(); // undo before returning to the parent
};
dfs(root);
return paths;
}Template 3: a global best updated at every node
Some paths bend: up one subtree, through a node, down the other.
Read more · 49 words
The answer is the best bend over all nodes, but a parent can only extend a straight arm. So at each node:
- record
leftArm + rightArmin an outerbest; - return only the longer arm.
Diameter of Binary TreeEasy uses heights as arms. Binary Tree Maximum Path SumHard uses sums, and treats a negative arm as 0.
// Template 3: return one thing up, record another in a variable outside the recursion.
// Longest path whose nodes all share one value, counted in edges. A path may bend at a node (left arm + right arm),
// but a parent can extend only ONE arm, so the helper returns the longer arm and the bent path goes into `best`.
export function longestUnivaluePath(root) {
let best = 0;
const arm = (node) => {
if (node === null) return 0;
const left = arm(node.left);
const right = arm(node.right);
const leftArm = node.left !== null && node.left.val === node.val ? left + 1 : 0;
const rightArm = node.right !== null && node.right.val === node.val ? right + 1 : 0;
best = Math.max(best, leftArm + rightArm); // the best path that bends exactly here
return Math.max(leftArm, rightArm); // what the parent is allowed to extend
};
arm(root);
return best;
}Template 4: BST bounds and ordering
In a BST, one comparison rules out a whole subtree: search and insert walk one branch, O(h).
Read more · 34 words
Validation is subtle. In [5, 4, 6, null, null, 3, 7] each node suits its parent, yet 3 sits right of 5. Carry (low, high) down, or check that the in-order walk strictly increases.
// BST template: in a binary search tree every value in a left subtree is smaller than the node and every value in a
// right subtree is larger. So one comparison tells you which whole subtree can't contain what you want: skip it.
export function rangeSum(root, low, high) {
if (root === null) return 0;
if (root.val < low) return rangeSum(root.right, low, high); // root and its entire left subtree are too small
if (root.val > high) return rangeSum(root.left, low, high); // root and its entire right subtree are too large
return root.val + rangeSum(root.left, low, high) + rangeSum(root.right, low, high);
}
// Walking down one branch needs no recursion at all: O(height).
export function searchBST(root, target) {
let node = root;
while (node !== null && node.val !== target) node = target < node.val ? node.left : node.right;
return node;
}Template 5: iterative DFS with an explicit stack
An array stack avoids call-stack overflow on very deep trees.
Read more · 33 words
- Pre-order: pop, visit, push the right child, then the left.
- In-order: slide left pushing every node, pop and visit one, then step right.
- Post-order: pre-order with the pushes swapped, then reverse the output.
Iterative DFS: in-order with an explicit stack
- node
- 6
Start with node at the root and an empty stack. The stack will hold the ancestors whose left side we are still exploring.
function inorder(root) {
const out = [], stack = [];
let node = root;
while (node !== null || stack.length) {
while (node !== null) {
stack.push(node);
node = node.left;
}
node = stack.pop();
out.push(node.val);
node = node.right;
}
return out;
}All steps as text
- Start with node at the root and an empty stack. The stack will hold the ancestors whose left side we are still exploring.
- Push 6: it must wait until everything smaller (its left subtree) is done. Slide left to 3.
- Push 3: it must wait until everything smaller (its left subtree) is done. Slide left to 1.
- Push 1. It has no left child, so nothing smaller is left to explore below it.
- Pop 1 and visit it: its whole left subtree is already in out, so it is the smallest value not yet output.
- 1 has no right subtree, so the next value to output is the one on top of the stack (3).
- Pop 3 and visit it: its whole left subtree is already in out, so it is the smallest value not yet output.
- Move into 3's right subtree (4): those values come after 3 and before anything still on the stack.
- Push 4. It has no left child, so nothing smaller is left to explore below it.
- Pop 4 and visit it: its whole left subtree is already in out, so it is the smallest value not yet output.
- 4 has no right subtree, so the next value to output is the one on top of the stack (6).
- Pop 6 and visit it: its whole left subtree is already in out, so it is the smallest value not yet output.
- Move into 6's right subtree (9): those values come after 6 and before anything still on the stack.
- Push 9: it must wait until everything smaller (its left subtree) is done. Slide left to 8.
- Push 8. It has no left child, so nothing smaller is left to explore below it.
- Pop 8 and visit it: its whole left subtree is already in out, so it is the smallest value not yet output.
- 8 has no right subtree, so the next value to output is the one on top of the stack (9).
- Pop 9 and visit it: its whole left subtree is already in out, so it is the smallest value not yet output.
- Move into 9's right subtree (10): those values come after 9 and before anything still on the stack.
- Push 10. It has no left child, so nothing smaller is left to explore below it.
- Pop 10 and visit it: its whole left subtree is already in out, so it is the smallest value not yet output.
- 10 has no right subtree and the stack is empty: there is nothing left to visit.
- node is null and the stack is empty, so every node was pushed once and popped once. out is sorted: [1, 3, 4, 6, 8, 9, 10].
// Iterative DFS: an explicit stack replaces the call stack. Same visiting order, no recursion depth limit.
export function preorderIterative(root) {
const out = [];
const stack = root ? [root] : [];
while (stack.length) {
const node = stack.pop();
out.push(node.val);
if (node.right) stack.push(node.right); // pushed first, so it is popped AFTER the left subtree
if (node.left) stack.push(node.left);
}
return out;
}
// In-order: slide left as far as possible, visit, then step into the right subtree. Sorted order on a BST.
export function inorderIterative(root) {
const out = [];
const stack = [];
let node = root;
while (node !== null || stack.length) {
while (node !== null) {
stack.push(node);
node = node.left;
}
node = stack.pop();
out.push(node.val);
node = node.right;
}
return out;
}
// Post-order: run pre-order with the children swapped (node, right, left), then reverse it (left, right, node).
export function postorderIterative(root) {
const out = [];
const stack = root ? [root] : [];
while (stack.length) {
const node = stack.pop();
out.push(node.val);
if (node.left) stack.push(node.left);
if (node.right) stack.push(node.right);
}
return out.reverse();
}Try it
// Edit the trees (level order, null = missing child) and press Run. The functions are the templates above.
const root = fromArray([8, 3, 10, 1, 6, null, 14, null, null, 4, 7]);
console.log(traversals(root)); // { pre: [8, 3, 1, 6, 4, 7, 10, 14], inorder: [1, 3, 4, 6, 7, 8, 10, 14], post: [1, 4, 7, 6, 3, 14, 10, 8] }
console.log(height(root)); // 4
console.log(goodNodes(fromArray([5, 3, 8, 6, 2, 7, 9]))); // 4
console.log(longestUnivaluePath(fromArray([1, 4, 5, 4, 4, 5]))); // 2
console.log(inorderIterative(fromArray([6, 3, 9, 1, 4, 8, 10]))); // [1, 3, 4, 6, 8, 9, 10]Complexity and JavaScript you'll use
Complexity
Read more · 134 words
| Variant | Time | Extra space |
|---|---|---|
| Recursive DFS, any order | O(n): each node entered once | O(h): O(log n) balanced, O(n) skewed |
| Global best (diameter, path sum) | O(n): one post-order pass | O(h) |
| Recomputing height at every node | O(n²) on a skewed tree | O(h): return heights instead |
| BST search or insert | O(h): one branch | O(1) iterative, O(h) recursive |
| BST range query | O(h + k) for k values in range | O(h) |
| Iterative DFS with a stack | O(n) | O(h) on the heap, no depth limit |
JavaScript you'll use
JavaScript refresher: Recursion, the call stack and memoization Linked list and tree nodes Functions, arrow functions and closuresconst dfs = (node, depth) => { // nested arrow function: sees outer variables
if (node === null) return 0; // base case first, always
return 1 + Math.max(dfs(node.left, depth + 1), dfs(node.right, depth + 1));
};
let best = -Infinity; // a global best lives in the closure, not in a parameter
const isLeaf = !node.left && !node.right; // a leaf has NO children, not "at least one missing"
[node.left, node.right] = [node.right, node.left]; // swap children in one lineInterview questions
Recursive or iterative DFS?
Recursive first. Then name the risk: a skewed tree of 10^5 nodes overflows the stack. An explicit stack fixes it.
How does the lowest common ancestor change for a BST?
Go left while both are smaller, right while both are larger. The split is the answer.
Why isn't the diameter always the path through the root?
The longest path may bend deep inside one subtree.
Can you traverse a tree in order with O(1) extra space?
Yes, with Morris traversal: temporary links back up the tree.
When should you switch from DFS to BFS on a tree?
For levels or the nearest match: see Tree BFS.
Check yourself
Question 1 of 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Maximum Depth of Binary TreeCount the nodes on the longest path from the root down to a leaf.Easy
- Invert Binary TreeMirror a binary tree by swapping the left and right child of every node.Easy
- Same TreeCheck whether two binary trees have the same shape and the same values at every position.Easy
- Diameter of Binary TreeFind the number of edges on the longest path between any two nodes of a binary tree.Easy
- Path SumDecide whether some root-to-leaf path adds up to a target sum.Easy
- Subtree of Another TreeCheck whether one binary tree appears, whole, as a subtree inside another.Easy
- Lowest Common Ancestor of a BSTUse the ordering of a binary search tree to find the lowest common ancestor of two values.Medium
- Validate Binary Search TreeCheck that every node is larger than everything in its left subtree and smaller than everything in its right subtree.Medium
- Kth Smallest Element in a BSTReturn the k-th smallest value in a binary search tree.Medium
- Lowest Common Ancestor of a Binary TreeGiven two values in a binary tree, return the value of their deepest shared ancestor.Medium
- Construct Binary Tree from Preorder and Inorder TraversalRebuild a binary tree from its preorder and inorder traversals.Medium
- Binary Tree Maximum Path SumFind the largest sum of node values along any path in a binary tree, where the path may start and end anywhere.Hard
- Average of Levels in Binary TreeReturn the average value of the nodes on each level of a binary tree, from the top level down.Easy
- Minimum Depth of Binary TreeCount the nodes on the shortest path from the root down to a leaf.Easy
- Binary Tree Right Side ViewList the values you would see standing to the right of a binary tree, one per level from top to bottom.Medium
In real systems
This pattern is not only for coding rounds. Here is where it does the work in a design interview.