Coding patternsTrees, graphs and heaps
Tree traversal
Binary tree traversal for coding interviews. Depth-first recursion that returns values up or passes ranges down, breadth-first level order, a step-through of max depth, code in Java, Python, C#, Go and TypeScript, and 10 LeetCode problems.
Tree problems scare people because recursion feels like magic. It stops feeling that way once you see that almost every tree problem is one of two questions:
- What does each subtree hand back to its parent? Its height, its sum, whether it’s balanced, whether it contains a target. Information flows up.
- What does each node need to know from above? The range its value must fall in, the path so far, its depth. Information flows down.
Answer that question, write the empty-tree case, and the recursion writes itself. The third tool is breadth-first traversal with a queue, for anything that talks about levels.
How to spot it
Section titled “How to spot it”Any problem on a binary tree, so the question is really which traversal:
- Depth, height, balance, diameter, path sums, lowest common ancestor: bottom-up DFS.
- Validating a BST, root-to-leaf paths, “good nodes”: top-down DFS, carrying state down.
- Level order, right side view, zigzag, minimum depth, anything “per level”: BFS.
- Sorted order from a BST, kth smallest: in-order DFS (left, node, right), which visits a BST’s values in increasing order.
Watch it run
Section titled “Watch it run”Max depth, bottom-up. Each call waits for both children. So the deepest nodes answer first, and the answers flow back up. The yellow nodes are on the call stack, still waiting. Green ones have returned, with the value they handed back underneath.
- def depth(node):
- if node is None: return 0
- left_depth = depth(node.left)
- right_depth = depth(node.right)
- return 1 + max(left_depth, right_depth) # combine
How deep is this tree? Ask each subtree, and combine the answers on the way back up.
The stack never holds more than the height of the tree: here 4 calls, for 7 nodes. On a balanced tree of a million nodes that’s about 20. On a tree that is one long chain, it’s a million, which is why the recursion limit comes up in interviews.
The template
Section titled “The template”Bottom-up depth, top-down BST validation, and BFS level order:
import java.util.ArrayDeque;import java.util.ArrayList;import java.util.List;import java.util.Queue;
public class TreeTraversal { record Node(int val, Node left, Node right) { Node(int val) { this(val, null, null); } }
static int maxDepth(Node node) { // Bottom-up: each subtree hands back one number, and the parent combines them. Write // the empty-tree case first, then trust the recursive calls. if (node == null) return 0; return 1 + Math.max(maxDepth(node.left()), maxDepth(node.right())); }
static boolean isBst(Node node, long low, long high) { // Top-down: the parent passes down the range every value below it must fall in. // Checking only left.val < val misses a grandchild on the wrong side of the root. // long bounds, so a node holding Integer.MAX_VALUE still fits strictly inside. if (node == null) return true; if (node.val() <= low || node.val() >= high) return false; return isBst(node.left(), low, node.val()) && isBst(node.right(), node.val(), high); }
static List<List<Integer>> levelOrder(Node root) { // Breadth-first: a queue visits nodes in order of depth. Reading the size before the // inner loop tells us exactly how many nodes make up the current level. List<List<Integer>> levels = new ArrayList<>(); Queue<Node> queue = new ArrayDeque<>(); if (root != null) queue.add(root); while (!queue.isEmpty()) { List<Integer> level = new ArrayList<>(); for (int remaining = queue.size(); remaining > 0; remaining--) { Node node = queue.poll(); level.add(node.val()); if (node.left() != null) queue.add(node.left()); if (node.right() != null) queue.add(node.right()); } levels.add(level); } return levels; }
public static void main(String[] args) { // 1 // 2 3 // 4 5 6 // 7 Node tree = new Node(1, new Node(2, new Node(4), new Node(5, new Node(7), null)), new Node(3, null, new Node(6))); check(maxDepth(tree) == 4); check(maxDepth(null) == 0); check(levelOrder(tree).equals(List.of(List.of(1), List.of(2, 3), List.of(4, 5, 6), List.of(7)))); check(isBst(new Node(5, new Node(3, new Node(1), new Node(4)), new Node(8)), Long.MIN_VALUE, Long.MAX_VALUE)); check(!isBst(new Node(5, new Node(3, null, new Node(6)), new Node(8)), Long.MIN_VALUE, Long.MAX_VALUE)); System.out.println("ok: maxDepth, isBst and levelOrder"); }
static void check(boolean ok) { if (!ok) throw new AssertionError(); }}from collections import dequefrom typing import Optional
class Node: def __init__(self, val: int, left: "Optional[Node]" = None, right: "Optional[Node]" = None): self.val, self.left, self.right = val, left, right
def max_depth(node: Optional[Node]) -> int: # Bottom-up: each subtree hands back one number, and the parent combines them. Write # the empty-tree case first, then trust the recursive calls. if node is None: return 0 return 1 + max(max_depth(node.left), max_depth(node.right))
def is_bst(node: Optional[Node], low: float = float("-inf"), high: float = float("inf")) -> bool: # Top-down: the parent passes down the range every value below it must fall in. Checking # only node.left.val < node.val misses a grandchild on the wrong side of the root. if node is None: return True if not low < node.val < high: return False return is_bst(node.left, low, node.val) and is_bst(node.right, node.val, high)
def level_order(root: Optional[Node]) -> list[list[int]]: # Breadth-first: a queue visits nodes in order of depth. Reading len(queue) before the # inner loop tells us exactly how many nodes make up the current level. levels: list[list[int]] = [] queue = deque([root] if root else []) while queue: level = [] for _ in range(len(queue)): node = queue.popleft() level.append(node.val) queue.extend(child for child in (node.left, node.right) if child) levels.append(level) return levels
# 1# 2 3# 4 5 6# 7tree = Node(1, Node(2, Node(4), Node(5, Node(7))), Node(3, None, Node(6)))assert max_depth(tree) == 4assert max_depth(None) == 0assert level_order(tree) == [[1], [2, 3], [4, 5, 6], [7]]assert is_bst(Node(5, Node(3, Node(1), Node(4)), Node(8)))assert not is_bst(Node(5, Node(3, None, Node(6)), Node(8))) # 6 sits left of 5print("ok: max_depth, is_bst and level_order")using System;using System.Collections.Generic;using System.Linq;
record Node(int Val, Node? Left = null, Node? Right = null);
static class TreeTraversal{ public static int MaxDepth(Node? node) { // Bottom-up: each subtree hands back one number, and the parent combines them. Write // the empty-tree case first, then trust the recursive calls. if (node is null) return 0; return 1 + Math.Max(MaxDepth(node.Left), MaxDepth(node.Right)); }
public static bool IsBst(Node? node, long low = long.MinValue, long high = long.MaxValue) { // Top-down: the parent passes down the range every value below it must fall in. // Checking only Left.Val < Val misses a grandchild on the wrong side of the root. if (node is null) return true; if (node.Val <= low || node.Val >= high) return false; return IsBst(node.Left, low, node.Val) && IsBst(node.Right, node.Val, high); }
public static List<List<int>> LevelOrder(Node? root) { // Breadth-first: a queue visits nodes in order of depth. Reading Count before the // inner loop tells us exactly how many nodes make up the current level. var levels = new List<List<int>>(); var queue = new Queue<Node>(); if (root is not null) queue.Enqueue(root); while (queue.Count > 0) { var level = new List<int>(); for (int remaining = queue.Count; remaining > 0; remaining--) { var node = queue.Dequeue(); level.Add(node.Val); if (node.Left is not null) queue.Enqueue(node.Left); if (node.Right is not null) queue.Enqueue(node.Right); } levels.Add(level); } return levels; }
static void Main() { // 1 // 2 3 // 4 5 6 // 7 var tree = new Node(1, new Node(2, new Node(4), new Node(5, new Node(7))), new Node(3, null, new Node(6))); Check(MaxDepth(tree) == 4); Check(MaxDepth(null) == 0); var levels = LevelOrder(tree).Select(level => string.Join(",", level)); Check(string.Join("|", levels) == "1|2,3|4,5,6|7"); Check(IsBst(new Node(5, new Node(3, new Node(1), new Node(4)), new Node(8)))); Check(!IsBst(new Node(5, new Node(3, null, new Node(6)), new Node(8)))); Console.WriteLine("ok: MaxDepth, IsBst and LevelOrder"); }
static void Check(bool ok) { if (!ok) throw new InvalidOperationException("check failed"); }}package main
import ( "fmt" "math" "reflect")
type Node struct { Val int Left, Right *Node}
func maxDepth(node *Node) int { // Bottom-up: each subtree hands back one number, and the parent combines them. Write the // empty-tree case first, then trust the recursive calls. if node == nil { return 0 } return 1 + max(maxDepth(node.Left), maxDepth(node.Right))}
func isBST(node *Node, low, high int) bool { // Top-down: the parent passes down the range every value below it must fall in. Checking // only Left.Val < Val misses a grandchild on the wrong side of the root. if node == nil { return true } if node.Val <= low || node.Val >= high { return false } return isBST(node.Left, low, node.Val) && isBST(node.Right, node.Val, high)}
func levelOrder(root *Node) [][]int { // Breadth-first: a queue visits nodes in order of depth. Here each level is its own // slice, so the next level is built while the current one is read. var levels [][]int current := []*Node{} if root != nil { current = append(current, root) } for len(current) > 0 { var nextLevel []*Node level := make([]int, 0, len(current)) for _, node := range current { level = append(level, node.Val) for _, child := range []*Node{node.Left, node.Right} { if child != nil { nextLevel = append(nextLevel, child) } } } levels = append(levels, level) current = nextLevel } return levels}
func main() { // 1 // 2 3 // 4 5 6 // 7 tree := &Node{1, &Node{2, &Node{Val: 4}, &Node{5, &Node{Val: 7}, nil}}, &Node{3, nil, &Node{Val: 6}}} check(maxDepth(tree) == 4) check(maxDepth(nil) == 0) check(reflect.DeepEqual(levelOrder(tree), [][]int{{1}, {2, 3}, {4, 5, 6}, {7}})) check(isBST(&Node{5, &Node{3, &Node{Val: 1}, &Node{Val: 4}}, &Node{Val: 8}}, math.MinInt, math.MaxInt)) check(!isBST(&Node{5, &Node{3, nil, &Node{Val: 6}}, &Node{Val: 8}}, math.MinInt, math.MaxInt)) fmt.Println("ok: maxDepth, isBST and levelOrder")}
func check(ok bool) { if !ok { panic("check failed") }}interface TreeNode { val: number; left?: TreeNode; right?: TreeNode;}
function maxDepth(node?: TreeNode): number { // Bottom-up: each subtree hands back one number, and the parent combines them. Write the // empty-tree case first, then trust the recursive calls. if (!node) return 0; return 1 + Math.max(maxDepth(node.left), maxDepth(node.right));}
function isBst(node?: TreeNode, low = -Infinity, high = Infinity): boolean { // Top-down: the parent passes down the range every value below it must fall in. Checking // only left.val < val misses a grandchild on the wrong side of the root. if (!node) return true; if (node.val <= low || node.val >= high) return false; return isBst(node.left, low, node.val) && isBst(node.right, node.val, high);}
function levelOrder(root?: TreeNode): number[][] { // Breadth-first: nodes in order of depth. Array.shift() is O(n), so instead of a queue we // build each level as its own array while reading the one before it. const levels: number[][] = []; let current = root ? [root] : []; while (current.length) { levels.push(current.map((node) => node.val)); current = current.flatMap((node) => [node.left, node.right].filter((child): child is TreeNode => !!child), ); } return levels;}
const check = (ok: boolean) => { if (!ok) throw new Error("check failed");};// 1// 2 3// 4 5 6// 7const tree: TreeNode = { val: 1, left: { val: 2, left: { val: 4 }, right: { val: 5, left: { val: 7 } } }, right: { val: 3, right: { val: 6 } },};check(maxDepth(tree) === 4);check(maxDepth(undefined) === 0);check(JSON.stringify(levelOrder(tree)) === "[[1],[2,3],[4,5,6],[7]]");check(isBst({ val: 5, left: { val: 3, left: { val: 1 }, right: { val: 4 } }, right: { val: 8 } }));check(!isBst({ val: 5, left: { val: 3, right: { val: 6 } }, right: { val: 8 } }));console.log("ok: maxDepth, isBst and levelOrder");Three habits keep tree recursion correct:
- Write the
Nonecase first. It’s almost alwaysreturn 0,return Trueorreturn None, and it stops the recursion. - Trust the recursive call. Assume
max_depth(node.left)returns the right answer for the left subtree. Don’t trace it in your head. Only check that you combine correctly. - Decide what flows which way before you code. Bounds go down as parameters. Heights come up as return values.
Returning one thing, recording another
Section titled “Returning one thing, recording another”Diameter of a binary tree is the longest path between any two nodes. The longest path through a
node is height(left) + height(right), but the parent needs that node’s height, not its
diameter. So the function returns the height and updates a best variable with
left + right along the way. Binary tree maximum path sum is the same shape with sums: return
the best one-sided path, and record the best two-sided one.
In-order for BSTs
Section titled “In-order for BSTs”An in-order traversal of a BST visits values in sorted order. Kth smallest is “stop at the kth visit”. Validate BST also has an in-order solution: check that each visited value is bigger than the previous one. It’s a nice alternative to passing bounds, and worth mentioning.
Lowest common ancestor
Section titled “Lowest common ancestor”Return the node if it’s one of the two targets, otherwise recurse both ways. If both sides return something, the current node is where the paths split: it’s the answer. If only one side does, pass that result up. Each node is visited once, so it’s O(n).
Complexity
Section titled “Complexity”| Time | Extra memory | |
|---|---|---|
| Any DFS | O(n) | O(h): about log n if balanced, n if it’s a chain |
| BFS | O(n) | O(w), the widest level: up to about n/2 |
Each subtree can report its height, and a node’s height is one more than its taller child, so I’ll recurse bottom-up. Every node is visited once: O(n) time, and O(h) stack.
Interview questions
Section titled “Interview questions”Interviewer asksWhat happens if the tree is a long chain of a million nodes?
Recursive DFS goes a million calls deep, and overflows the stack. Python’s default limit is 1,000 calls. Java and C# stacks typically hold tens of thousands of frames. Fix it with an explicit stack (iterative DFS), or with BFS. Go’s stacks grow on demand, so it survives longer. But memory is still O(n).
Interviewer asksMinimum depth: why is BFS better than DFS here?
BFS visits levels in order, so the first leaf it reaches is at the minimum depth and it can stop there. DFS has to explore the whole tree to be sure. On a tree with a shallow leaf and a huge deep subtree, BFS does far less work.
Interviewer asksSerialize and deserialize a binary tree: which traversal would you use?
Use pre-order (node, left, right), with a marker for empty children, such as 1,2,#,#,3,#,#.
Pre-order puts each root before its subtrees. So deserializing is the same recursion in reverse.
Read a value, and build the node. Then build its left and right subtrees from what follows. The
markers are what make the tree unambiguous.
Practice
Section titled “Practice”10 problems on LeetCode: 4 easy, 5 medium, 1 hard
- Maximum Depth of Binary Tree(opens LeetCode in a new tab)Easy
Each subtree returns its depth; the parent adds one.
- Invert Binary Tree(opens LeetCode in a new tab)Easy
Swap the children, then let recursion handle each side.
- Same Tree(opens LeetCode in a new tab)Easy
Walk both trees together and compare node by node.
- Diameter of Binary Tree(opens LeetCode in a new tab)Easy
Return the height, but record left plus right height on the way.
- Binary Tree Level Order Traversal(opens LeetCode in a new tab)Medium
A queue, processed one level size at a time.
- Binary Tree Right Side View(opens LeetCode in a new tab)Medium
The last node of each BFS level is the one you can see.
- Validate Binary Search Tree(opens LeetCode in a new tab)Medium
Pass down the allowed range, not just the parent value.
- Kth Smallest Element in a BST(opens LeetCode in a new tab)Medium
In-order traversal of a BST visits values in sorted order.
- Lowest Common Ancestor of a Binary Tree(opens LeetCode in a new tab)Medium
The first node where the two targets come back from different sides.
- Binary Tree Maximum Path Sum(opens LeetCode in a new tab)Hard
Return the best one-sided path; record the best two-sided one.
Ticks are saved in this browser only. Clear your site data and they're gone.
- Ask first: what flows up (return values) and what flows down (parameters)?
- DFS:
Nonecase, recurse, combine. BFS: queue, one level at a time. - In-order on a BST gives sorted order.
- When the answer isn’t the return value, return what the parent needs and record the answer on the side.
- Cost: O(n) time. O(h) memory for DFS, and O(width) for BFS. Deep chains overflow recursion.
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.