DSA sheet · Trees · DFS pattern
Maximum Depth & Minimum Depth of a Binary Tree
Two very common warm-up problems, taught together because they look almost the same. The teacher solves maximum depth with BFS (count the levels) and DFS (ask each child for its height). Then she tries the obvious shortcut for minimum depth, changing max to min, and shows on screen that it gives a wrong answer. Fixing that mistake is the most important lesson here: an empty child is not a leaf. Finally she shows BFS makes minimum depth easy: stop at the first leaf you meet.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · What you must know before starting
- Part A · Maximum depth with BFS (count the levels)
- Part B · Maximum depth with DFS (max of the children + 1)
- Part C · Minimum depth with DFS (the max → min trap, and the fix)
- Part D · Minimum depth with BFS (stop at the first leaf)
- Part E · Revision page
Part 0 · Before starting
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = rightWords we need
- Leaf: a node with no children at all (both left and right are None).
- Level: a row of the tree. The root is on the first row, its children on the second, and so on.
- Depth of the tree (on LeetCode): the number of nodes on a path that goes from the root down to a leaf. A single node has depth 1, and an empty tree has depth 0.
- Maximum depth: the longest such path. The teacher says this is the same thing as the height of the tree, and also the same as the number of levels.
- Minimum depth: the shortest path from the root down to the nearest leaf.
3
/ \
9 20
/ \
15 7 1
\
2In example 2, [1, null, 2] means 1 has no left child and has 2 as its right child. There are 2 levels, so the maximum depth is 2.
Part A · Maximum depth with BFS
LeetCode 104 · Maximum Depth of Binary Tree
1The question in simple words
Given the root, return the number of nodes on the longest path from the root down to a leaf. This is the teacher's own tree for this problem:
1 level 1
/ \
2 3 level 2
/ \
4 5 level 3
/
6 level 4
The longest path is 1 → 3 → 5 → 6, which has 4 nodes. Answer: 4.
2What the constraints tell us
- Number of nodes: 0 to 104.
- 0 allowed → in BFS we must write the empty check first (return 0).
- In DFS the base case is needed no matter what the minimum is, because it's the only thing that stops the recursion.
- n up to 104 → an O(n²) idea would be 104 × 104 = 108 steps. The teacher's rule: about 108 is already slow and beyond it you risk TLE. So we want O(n).
- Values: −100 to 100. We'd only care about the range if we were adding or multiplying values (int holds about ±2 × 109; beyond that you need long). Here we never touch the values at all, so it's safe.
3Intuition
Maximum depth = number of levels. BFS level order already walks the tree one level at a time: each round of the outer while loop handles exactly one level. So we just count the rounds.
4Building the logic: edit level order
- Same queue, same
size = len(queue), same inner loop that pops the level and pushes children. - We don't store anything: no sublist, no values. We only need the children to go into the queue.
- Keep a counter
depth = 0. After each inner loop (one full level done), dodepth += 1. - When the queue becomes empty,
depthis the number of levels.
depth += 1 after the for loop and not inside it?→ Inside the for loop, it would add 1 for every node, and we'd be counting nodes (6), not levels (4). One round of the outer loop = one level, so the counter goes outside the inner loop.
5Approach steps
- If the root is None, return 0.
- Put the root in the queue;
depth = 0. - While the queue isn't empty:
size = len(queue); popsizenodes and push their children. - After each level,
depth += 1. - Return
depth.
6Code (Python)
from collections import deque
class Solution:
def maxDepth(self, root):
if root is None: # 0 nodes allowed
return 0
queue = deque([root])
depth = 0
while queue:
size = len(queue)
for i in range(size): # pop one whole level
node = queue.popleft()
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
depth += 1 # one more level finished
return depth7Code line by line
| line | what it means |
|---|---|
| if root is None: return 0 | An empty tree has no levels. |
| depth = 0 | No levels counted yet. |
| size = len(queue) | The queue holds exactly one level right now. |
| for i in range(size): ... | Pop that level and push the next level. We don't use the values. |
| depth += 1 | Count the level we just finished. |
8Dry run on the teacher's tree
9Complexity & remember
- Time O(n): every node is pushed and popped once.
- Space O(n) worst case. The teacher's picture: a complete tree with 15 nodes has 8 on the last level, and at one point all 8 sit in the queue together. That's about n/2, which is O(n).
1
/ \
2 3
/ \ / \
4 5 6 7
/ \ / \ / \ / \
8 9 10 11 12 13 14 15 ← 8 of 15 nodes in the queue at once
depth += 1 after each level.Part B · Maximum depth with DFS
1The question, and 2 the constraints
Same question and constraints as Part A. This is recursion, so the base case is mandatory.
3Intuition: ask your children
Stand at the root. From there you can't see how deep the tree goes. But you can ask your left child "how tall is your side?" and your right child "how tall is yours?". In the teacher's tree, the left side (just 2) answers 1, and the right side (3, 4, 5, 6) answers 3.
Your own height is then the taller of the two answers, plus 1 for yourself: max(1, 3) + 1 = 4. Every node does the same thing, asking its own children. That's a recursive function.
height(node) = max(height(left), height(right)) + 1
4Building the logic
The base case: what does an empty spot return?
The teacher's habit: whenever you write a DFS function, start with "what if the node is None?". Since our function returns a height, the answer must be an integer, not a boolean. An empty spot adds no nodes to any path → return 0.
The recursive part
The calls return integers, so store them: left = self.maxDepth(root.left), right = self.maxDepth(root.right).
Max or sum? The teacher asks this explicitly
Look at node 2: left gives 0, right gives 0. What should 2 report to its parent? A node can only hand back one path going down from it. Node 3 has two paths below it (through 4 and through 5), but it can only return one of them, so it returns the longer. Adding them would count two different paths as if they were one. So it's max, not sum, plus 1 for the node itself.
→ When the question asks about a path that goes through a node from one side to the other, like the diameter problem later in the sheet. For the depth from the root down, it's always one side, so max.
5Approach steps
- If the node is None, return 0.
- Get the left height by recursion.
- Get the right height by recursion.
- Return
max(left, right) + 1.
6Code (Python)
class Solution:
def maxDepth(self, root):
if root is None: # empty spot adds nothing
return 0
left = self.maxDepth(root.left) # height of the left side
right = self.maxDepth(root.right) # height of the right side
return max(left, right) + 1 # taller side + me7Code line by line
| line | what it means |
|---|---|
| if root is None: return 0 | Base case: stops the recursion, and makes an empty tree give 0. |
| left = self.maxDepth(root.left) | Python pauses here until the whole left side has reported its height. |
| right = self.maxDepth(root.right) | Then the right side. |
| return max(left, right) + 1 | The longer side, plus this node. |
The main function is the DFS itself here. With a separate helper, the main function would just return dfs(root).
8Dry run with the call stack
- f(1) → go left: f(2) → go left: f(None) → 0, leaves the stack. f(2) right: f(None) → 0.
- f(2) has left 0, right 0 → max(0, 0) + 1 = 1. 2 leaves the stack; f(1) now has left = 1.
- f(1) goes right: f(3) → left: f(4) → its children give 0 and 0 → returns 1. f(3) has left = 1.
- f(3) goes right: f(5) → left: f(6) → children 0, 0 → returns 1. f(5) has left = 1.
- f(5) right: f(None) → 0. f(5) returns max(1, 0) + 1 = 2 to f(3), as its right value. (In the video she first says "six's right", then corrects herself: it's 3's right.)
- f(3) has left 1, right 2 → max + 1 = 3 → f(1)'s right = 3.
- f(1): max(1, 3) + 1 = 4. The stack is empty; 4 goes back to the caller ✓
9Complexity & remember
- Time O(n): every node is visited once.
- Space O(height), for the call stack. In the dry run the stack was at most 1, 3, 5, 6: four calls, one per level. For a balanced tree the number of levels is about log n. For a skewed tree (all nodes leaning to one side, like a chain) the number of levels is about n, so the worst case is O(n).
None → 0 · max(left, right) + 1. Max, not sum: a node reports only one path.Part C · Minimum depth with DFS
LeetCode 111 · Minimum Depth of Binary Tree
1The question in simple words
Return the number of nodes on the shortest path from the root down to a leaf. Remember, a leaf is a node with no children.
3
/ \
9 20 9 is a leaf: 3 → 9 = 2 nodes
/ \
15 7 2
\
3
\
4
\
5
\
6 the only leaf is 6: 5 nodes2What the constraints tell us
- 0 nodes allowed → the empty tree must return 0. In DFS the base case does that.
- Up to 105 nodes on LeetCode → O(n) needed.
→ A chain like example 2 but with thousands of nodes means thousands of nested calls. Python's default recursion limit is about 1000, so a very deep skewed tree can raise
RecursionError. BFS (Part D) has no such limit. Good to mention in an interview.3Intuition: first try the obvious copy
Minimum depth sounds like max depth with max swapped for min. The teacher does exactly that: copies the max depth code, renames it, changes max to min, and runs it.
class Solution:
def minDepth(self, root):
if root is None:
return 0
left = self.minDepth(root.left)
right = self.minDepth(root.right)
return min(left, right) + 1 # WRONG when one child is missingExample 1 passes (2). Example 2 fails: it returns 1, but the answer is 5.
4Building the logic: why it fails, and the fix
What goes wrong on example 2
Stand at the root 2. Its left child is None, so the left call returns 0. The right side (3, 4, 5, 6) returns 4. The code returns min(0, 4) + 1 = 1. It's acting as if the path "2 → nothing" ended at a leaf after 1 node.
But a missing child is not a leaf. A leaf is a real node with no children. Root 2 has a child, so 2 is not a leaf, and no path can stop at 2. The 0 from the empty side is not a path at all, so it must not win the min.
→ In max, a 0 from an empty side can never win when the other side is bigger, so it's ignored automatically. And when both sides are 0, the node really is a leaf, so 0 + 1 = 1 is correct. Only
min is fooled, because it prefers the smallest number, and 0 is the smallest.The fix: only compare when both children exist
- No left child → every path must go right → return
minDepth(right) + 1. - No right child → every path must go left → return
minDepth(left) + 1. - Both children exist → now both sides are real paths → return
min(left, right) + 1.
→ The first check fires (left is None) and returns
minDepth(None) + 1 = 0 + 1 = 1. That's right: a leaf alone is a path of 1 node. So we don't need a separate leaf check.if root is None: return 0?→ Yes. It handles the empty tree (answer 0), and it's what the one-child cases call into, like
minDepth(None) + 1 at a leaf.5Approach steps
- If the node is None, return 0.
- If the left child is None, return
minDepth(right) + 1. - If the right child is None, return
minDepth(left) + 1. - Otherwise return
min(minDepth(left), minDepth(right)) + 1.
6Code (Python)
class Solution:
def minDepth(self, root):
if root is None:
return 0
if root.left is None: # only the right side has paths
return self.minDepth(root.right) + 1
if root.right is None: # only the left side has paths
return self.minDepth(root.left) + 1
left = self.minDepth(root.left) # both exist: compare them
right = self.minDepth(root.right)
return min(left, right) + 17Code line by line
| line | what it means |
|---|---|
| if root is None: return 0 | Empty tree, or the empty side used by the next two checks. |
| if root.left is None: return self.minDepth(root.right) + 1 | No left child → the nearest leaf must be on the right. This also covers leaves (gives 1). |
| if root.right is None: return self.minDepth(root.left) + 1 | No right child → the nearest leaf must be on the left. |
| return min(left, right) + 1 | Both sides are real; take the shorter one, plus this node. |
8Dry run on example 2
- f(2): left is None → return f(3) + 1. (The wrong version would have used the 0 here.)
- f(3): left is None → return f(4) + 1.
- f(4): left is None → f(5) + 1. f(5): left is None → f(6) + 1.
- f(6): a leaf → left is None → f(None) + 1 = 0 + 1 = 1.
- Coming back up: f(5) = 2, f(4) = 3, f(3) = 4, f(2) = 5 ✓
On example 1: f(3) has both children → min(f(9), f(20)) + 1. f(9) is a leaf → 1. f(20) → both children are leaves → min(1, 1) + 1 = 2. So f(3) = min(1, 2) + 1 = 2 ✓
9Complexity & remember
- Time O(n): each node is visited once.
- Space O(height) for the call stack: about log n if balanced, O(n) if skewed (like example 2).
min only when both children exist.Part D · Minimum depth with BFS
1The question, and 2 the constraints
Same as Part C. 0 nodes allowed, so BFS needs the empty check: return 0.
3Intuition: the first leaf you meet is the answer
The teacher says this is even easier with BFS. BFS visits the tree level by level, from the top. So the first leaf it pops is on the shallowest level that has any leaf. Return that level's number right there, and don't look any further.
Her example: take example 2's chain and add one extra leaf, 5, as the left child of the root:
2 level 1
/ \
5 3 level 2 ← 5 is a leaf → answer 2
\
4
\
5
\
6 (the chain's leaf, level 5)
Pop 2: not a leaf, push 5 and 3. Next level: pop 5: leaf → return 2. We never even look at 4, the second 5, or 6. Without the leaf check, BFS would keep going all the way down to 6 and count 5 levels, which is the maximum depth.
For the plain chain in example 2, the only leaf is 6 at the bottom, so here the minimum and maximum depth are the same: 5.
4Building the logic: edit max depth BFS
- Copy the max depth BFS code.
- Start
depth = 1, not 0, because the root we put in the queue is already on level 1. - Right after popping a node, check:
node.left is None and node.right is None? If so, it's a leaf →return depth. - Otherwise push its children as usual, and after each level
depth += 1. - The final
returnafter the loop is never reached for a non-empty tree (every tree has at least one leaf). Java needs a return there to compile; in Python it's just a safety line.
→ Checking when we pop means we check each node while we're on its level, so
depth is that node's level. Either place can work if you're careful with the counting, but this way is the simplest. Also, this BFS can be faster than DFS for min depth: it stops at the first leaf, while DFS has to explore every branch before it knows the minimum.5Approach steps
- If the root is None, return 0.
- Queue = [root],
depth = 1. - While the queue isn't empty: pop the whole level. For each node, if it's a leaf, return
depth; else push its children. - After each level,
depth += 1.
6Code (Python)
from collections import deque
class Solution:
def minDepth(self, root):
if root is None:
return 0
queue = deque([root])
depth = 1 # the root is level 1
while queue:
size = len(queue)
for i in range(size):
node = queue.popleft()
if node.left is None and node.right is None:
return depth # first leaf = shallowest leaf
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
depth += 1
return depth # never reached for a real tree7Code line by line
| line | what it means |
|---|---|
| depth = 1 | We start on the root's level, which counts as 1. |
| if node.left is None and node.right is None: return depth | A leaf on this level. Since BFS goes top-down, no leaf can be higher, so this is the answer. |
| queue.append(node.left / right) | Not a leaf → its children go on to the next level. |
| depth += 1 | The whole level had no leaf; move one level down. |
8Dry run
Her tree with the extra leaf 5
LeetCode example 1
Example 2 (the chain)
9Complexity & remember
- Time O(n) in the worst case (when the first leaf is deep). Often much less, because it stops early.
- Space O(n) worst case for the queue (the widest level).
depth = 1. Pop a node: if it's a leaf, return depth. The first leaf BFS meets is the shallowest one.Part E · Revision page
| max depth | min depth | |
|---|---|---|
| meaning | longest root-to-leaf path (number of levels) | shortest root-to-leaf path |
| BFS | count every level, depth starts at 0 | stop at the first leaf, depth starts at 1 |
| DFS base case | None → 0 | None → 0 |
| DFS combine | max(l, r) + 1 always | one child missing → other side + 1; else min(l, r) + 1 |
| why the difference | a 0 never wins a max unless it's a real leaf | a 0 from a missing child would win a min, wrongly |
| time / space | O(n) / BFS O(n), DFS O(height) | O(n) / BFS O(n), DFS O(height) |
| BFS | DFS | |
|---|---|---|
| empty-tree check | needed only if 0 nodes allowed | always (base case) |
| extra memory | widest level (≈ n/2 for a full tree) | tallest path (log n balanced, n skewed) |
| good for | min depth (stops at the first leaf) | max depth (short, clean code) |
2. BFS max:
depth += 1 after each level.3. DFS max:
None → 0, max(left, right) + 1.4. DFS min: if one child is missing, go to the other side;
min only when both exist.5. BFS min: the first leaf you pop gives the answer.
max to min and nothing else (fails on one-child nodes)✗ treating None as a leaf
✗
depth += 1 inside the inner loop (counts nodes, not levels)✗ starting BFS min depth at 0
✗ adding left and right heights instead of taking the max
t = TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4), TreeNode(5, TreeNode(6)))) ex1 = TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7))) chain = TreeNode(2, None, TreeNode(3, None, TreeNode(4, None, TreeNode(5, None, TreeNode(6))))) s = Solution() print(s.maxDepth(t)) # 4 print(s.maxDepth(ex1)) # 3 print(s.minDepth(ex1)) # 2 print(s.minDepth(chain)) # 5 print(s.minDepth(None)) # 0
Based on this video: Maximum & Minimum Depth of Binary Tree | DFS & BFS