DSA sheet · Trees · DFS pattern

Diameter of Binary Tree

The teacher calls this a very common question for freshers. She solves it twice: first with BFS, which is easy to think of but slow (O(n²)), and then with DFS, which does the same job in one pass, O(n). The big lesson is one DFS can do two jobs at once: return a height to the parent, and quietly update the best answer on the side. This trick comes back again in Balanced Binary Tree and Maximum Path Sum, so learn it well here.

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 · Words you must know first

wordmeaning in simple words
edgeThe line that joins a parent to a child. A tree with n nodes has n − 1 edges.
pathA walk from one node to another along edges, never visiting a node twice. It can go up and then down, but it can never split into two branches.
length of a pathThe number of edges on it, not the number of nodes. A path through 7 nodes has length 6.
levelA "row" of the tree. The root is on the first level, its children on the second, and so on.
height (as used in this video)The number of levels in a tree, which is the number of nodes on the longest root-to-bottom path. An empty tree has height 0, a single node has height 1.
subtreeA node together with everything below it. "The left subtree of 1" means node 2 and all of 2's descendants.

Part A · Diameter with BFS (brute force)

LeetCode 543

1The question in simple words

You get the root of a binary tree. Return its diameter: the length (number of edges) of the longest path between any two nodes. The path does not have to go through the root.

This is the tree the teacher uses for the whole video:

              1
            /   \
           2     3
         /   \
        4     5
       /     / \
      9     6   7
       \       /
        10    8

Let's stand on each node and ask: "If the path bends at me, how far can it go down on my left, and how far on my right?" Add the two, and you get the longest path that bends at this node.

nodelongest way down on the left (edges)longest way down on the right (edges)left + right
14 (1→2→4→9→10)1 (1→3)5
23 (2→4→9→10)3 (2→5→7→8)6 ← biggest
3000
42 (4→9→10)02
901 (9→10)1
51 (5→6)2 (5→7→8)3
71 (7→8)01
10, 6, 8000 (leaves)

The biggest value is at node 2: the path 10 – 9 – 4 – 2 – 5 – 7 – 8. It touches 7 nodes but has 6 edges, so the answer is 6.

Doubt: node 1 has three ways to go down on its left (1→2→4→9→10, 1→2→5→6, 1→2→5→7→8). Which one counts?
→ Only the longest one. We want the longest path, so on each side we keep the deepest way down. For 1's left that is 4 edges (two routes tie at 4, either is fine).
"May not pass through the root"The answer path 10…8 bends at node 2 and never touches node 1. So we can't just look at the root. We must check every node as the possible bending point.

2What the constraints tell us

3Intuition: how to think about it

From the table above, the plan is clear: for every node, find the height of its left subtree and the height of its right subtree, add them, and keep the biggest sum.

The BFS way to do that: walk through all nodes level by level using a queue. For each node we take out, ask a helper "how tall is my left subtree? how tall is my right subtree?" The helper itself does a level-order traversal and simply counts the levels.

4Building the logic from examples

Why "levels" of a subtree = "edges" on that side

Take node 1. Its left subtree starts at 2 and has 4 levels: (2), (4, 5), (9, 6, 7), (10, 8). The longest way down on 1's left is 1→2→4→9→10, which is 4 edges. The same number!

Why? If you count only the edges inside the subtree (2→4→9→10) you get 3. But the path from 1 also uses the edge 1→2. Counting levels (nodes) of the subtree instead of edges inside it adds exactly that one missing edge. So:

Key factheight(node.left) = number of edges from node down to the deepest point on its left.
So height(node.left) + height(node.right) = the longest path that bends at node.

Check for node 1: left height 4 + right height 1 (just node 3) = 5, matching the table.

The outer BFS: visit every node

Doubt 1: in Same Tree BFS we pushed None children too. Why push only real children here?
→ Here we aren't pairing nodes. Every popped node is sent to height() and we read node.left from it. If we popped a None, None.left would crash (in Java that's the null pointer exception the teacher warns about). So only push children that exist.
Doubt 2: the teacher says "call height with root.left" inside the loop. Is it really the root?
→ No, it must be the node we just popped: node.left and node.right. Using root every time would compute only the root's value again and again. The notes use node.

The helper height(): level order, but just count

This is the level-order traversal from the earlier video, with one change. There we stored every level in its own sub-list, and the number of sub-lists was the height. That wastes space. Here we don't store anything: we just add 1 to a counter each time a full level is finished.

A small practice tree the teacher uses:

      2
     / \
    3   4
       / \
      5   6
Doubt 3: why take size = len(queue) before the inner loop?
→ At that moment the queue holds exactly one level. While we pop, we keep pushing the next level's nodes at the back. Fixing the size first means the inner loop pops only this level and stops, so one round of the inner loop = one level = height += 1.

5Approach steps

  1. If the tree is empty, return 0. Set diameter = 0 and put the root in a queue.
  2. While the queue is not empty, pop a node.
  3. Find left = height(node.left) and right = height(node.right) with a level-order count.
  4. Set diameter = max(diameter, left + right).
  5. Push the node's children that exist.
  6. When the queue is empty, return diameter.

6Code (Python)

Diameter with BFS (brute force)
from collections import deque

class Solution:
    def diameterOfBinaryTree(self, root):
        if root is None:                       # safety guard
            return 0
        diameter = 0
        queue = deque([root])
        while queue:
            node = queue.popleft()
            left = self.height(node.left)      # levels on the left
            right = self.height(node.right)    # levels on the right
            diameter = max(diameter, left + right)
            if node.left:                      # push only real children
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        return diameter

    def height(self, root):                    # level order, only counting
        if root is None:
            return 0
        queue = deque([root])
        height = 0
        while queue:
            size = len(queue)                  # nodes on this level
            for _ in range(size):
                node = queue.popleft()
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            height += 1                        # one full level finished
        return height

7Code line by line

linewhat it means
if root is None: return 0An empty tree has no path. The constraints say n ≥ 1, so this is only a guard.
diameter = 0The best path length found so far. Every real answer is ≥ 0, so 0 is a safe start.
node = queue.popleft()Take the next node to test as a "bending point".
left = self.height(node.left) right = self.height(node.right)Ask how far we can go down on each side, counted as levels (= edges from node).
diameter = max(diameter, left + right)Keep the longer of: the best so far, or the path bending here.
if node.left: queue.append(...)Queue up the children so every node gets checked. Never push None.
size = len(queue)In the helper: freeze the size of the current level.
for _ in range(size): ...Pop exactly this level's nodes and push their children (the next level).
height += 1One level done. We don't store the nodes, only count levels.
return heightNumber of levels = height of this subtree.

8Dry run: watch the outer queue

On the main tree. Each line pops one node and calls the helper twice.

start1diameter = 0
pop 1height(2) = 4, height(3) = 1 → 4 + 1 = 5 → diameter = 5. Push 2, 3.
23
pop 2height(4) = 3, height(5) = 3 → 6 → diameter = 6. Push 4, 5.
345
pop 30 + 0 = 0 → stays 6. No children.
pop 4height(9) = 2, height(None) = 0 → 2 → stays 6. Push 9.
59
pop 5height(6) = 1, height(7) = 2 → 3 → stays 6. Push 6, 7.
pop 9, 6, 7, 10, 8sums 1, 0, 1, 0, 0 → never beat 6.
endqueue empty → return 6 ✓

Notice how the helper re-walks the same nodes again and again: node 10 is counted when we pop 1, again when we pop 2, again for 4, and again for 9. That repeated work is what makes this slow.

9Complexity & remember

Remember the BFS ideaFor each node: diameter candidate = height(left) + height(right), with heights counted as levels. Correct, but it recounts the same subtrees again and again → O(n²).

Part B · Diameter with DFS (optimal)

1The question (same as Part A)

Same tree, same answer (6). The meaning of diameter doesn't change. Only the way we collect the heights changes.

2What the constraints tell us

3Intuition: the answer comes from below

Standing on node 1, can we decide anything about its diameter? No. We need the heights from below first. So we shouldn't do work at the node and then go down (like preorder). We should go down first, collect the answers from the children, and only then decide at the node. That's postorder thinking: left, right, then the node.

And here is the saving: when a child finishes, it already knows its own height. It just hands that number up. The parent never has to re-walk the subtree like the BFS helper did. Each node is visited once.

4Building the logic from examples

Base case: what does an empty child return?

The parent wants to add the left answer and the right answer. If an empty child returned None, how would node 3 add None + None? It can't. So an empty spot returns 0: "no levels here".

Doubt: how do I choose what a base case returns?
→ Look at what the parent does with it. If the parent adds numbers, return a number that doesn't disturb the sum: 0. If the function returns True/False, return a boolean. The teacher repeats this rule often.

At a node: update the diameter

Look at leaf 10: left gives 0, right gives 0. Forget the rest of the tree. Just this one node has no edges, so its diameter is 0 + 0 = 0. In general, once we have left and right:

Job 1: update the global answerdiameter = max(diameter, left + right)
Same formula as BFS. The path bending at this node has left + right edges.

What should the node return to its parent? Not the diameter!

Try returning the diameter. Leaf 10 would return 0 to node 9. But for 9, the right side really has 1 edge (9→10). With 0, node 9 would think its diameter is 0, which is wrong. So the return value must be something else.

Now look at node 2 from its parent 1's point of view. Below 2 there are many paths: one through 4 (to 10) and two through 5 (to 6, and to 8). But node 1 can extend only one of them, because a path can't split into two branches. So 2 should hand up the longest single branch.

Job 2: return the heightreturn max(left, right) + 1
"My tallest side, plus me."
Doubt: leaf 10 returns max(0, 0) + 1 = 1. Is 1 right?
→ Yes. It means "one level below you, parent". For node 9 that's exactly 1 edge on the right (9→10). This is the same "levels = edges from the parent" fact from Part A.

So one function does two jobs: it returns the height (needed by the parent) and it updates the diameter in a variable outside the function (needed for the final answer). The value returned by the very first call isn't the answer, so we ignore it and return the diameter variable.

5Approach steps

  1. Set diameter = 0 outside the helper. Call height(root), then return diameter.
  2. In height(node): if the node is None, return 0.
  3. Get left = height(node.left) and right = height(node.right).
  4. Update diameter = max(diameter, left + right).
  5. Return max(left, right) + 1.

6Code (Python)

Diameter with DFS (optimal)
class Solution:
    def diameterOfBinaryTree(self, root):
        self.diameter = 0                # best path length seen so far
        self.height(root)                # fills self.diameter
        return self.diameter

    def height(self, root):
        if root is None:                 # base case: no levels
            return 0
        left = self.height(root.left)    # height of left subtree
        right = self.height(root.right)  # height of right subtree
        self.diameter = max(self.diameter, left + right)   # job 1
        return max(left, right) + 1                         # job 2

In Java the teacher keeps diameter as a class field. In Python we use self.diameter and reset it at the top of the main method, so calling the method twice on the same object can't mix answers.

7Code line by line

linewhat it means
self.diameter = 0A "notice board" outside the recursion. Every node can write a better answer on it.
self.height(root)Start the DFS. Its return value (the tree's height) isn't what we want, so we don't store it.
if root is None: return 0The base case. Stops the recursion and gives the parent a number it can add.
left = self.height(root.left)Go fully down the left first. Python pauses here until the whole left side is done.
right = self.height(root.right)Then the right side.
self.diameter = max(...)The path bending here is left + right edges. Keep it if it's the best.
return max(left, right) + 1Give the parent only the longest single branch, plus this node.

8Dry run using the call stack

Main tree again. Each line shows (left, right) → what's written on the board → what's returned.

  1. Calls go down the left edge: h(1) → h(2) → h(4) → h(9) → h(9.left = None) returns 0.
  2. h(9) goes right: h(10). Both its children are None → left 0, right 0. Board: max(0, 0) = 0. Returns max(0,0)+1 = 1.
  3. Back in h(9): left 0, right 1. Board: max(0, 1) = 1. Returns 1 + 1 = 2.
  4. h(4): left 2 (from 9), right 0 (None). Board: max(1, 2) = 2. Returns 2 + 1 = 3.
  5. h(2) now goes right: h(5) → h(6): leaf → board stays 2, returns 1.
  6. h(5) goes right: h(7) → h(8): leaf → returns 1. h(7): left 1, right 0 → board stays 2 (1 < 2). Returns 2.
  7. h(5): left 1, right 2 → board: max(2, 3) = 3. Returns 2 + 1 = 3.
  8. h(2): left 3, right 3 → board: max(3, 6) = 6. Returns 3 + 1 = 4 (only one branch goes up).
  9. h(1) goes right: h(3): leaf → board stays 6, returns 1.
  10. h(1): left 4, right 1 → 5, board stays 6. Returns 5, which nobody needs. Final answer: diameter = 6 ✓
step 2 (deepest)
h(1)h(2)h(4)h(9)h(10) → 1
step 6
h(1)h(2)h(5)h(7)h(8) → 1
step 8
h(1)h(2): board = 6, → 4

The newest call is on top (red). Each call leaves the stack the moment it returns its height.

9Complexity & remember

Remember Diameter (DFS)Base: None → 0. At each node: board = max(board, L + R), then return max(L, R) + 1. Return the board, not the function's result.

Part C · Revision page

BFS (Part A)DFS (Part B)
how heights are founda separate level-order count for every nodechildren return their height to the parent
diameter updatemax(d, height(L) + height(R))max(d, left + right)
visits per nodemany (once per ancestor, plus itself)exactly one
base caseguard only (n ≥ 1)needed: None → 0
push None children?no (would crash on .left)n/a
time / spaceO(n²) / O(2n)O(n) / O(n)
whatvaluewhy
written on the boardleft + righta path can bend at this node and use both sides
returned to the parentmax(left, right) + 1the parent can extend only one branch
If you remember only 5 lines 1. Diameter = most edges on any path. It may skip the root.
2. Every node is a possible bending point: candidate = height(left) + height(right).
3. Height in levels = edges from the parent's side, so the plain sum is already in edges.
4. DFS: None → 0, update the board with L + R, return max(L, R) + 1.
5. BFS re-measures heights for every node → O(n²). DFS does it once → O(n).
Mistakes to avoid ✗ counting nodes instead of edges (7 instead of 6)
✗ only checking the root as the bending point
✗ returning the diameter (or L + R) to the parent instead of max(L, R) + 1
✗ returning None from the base case (can't add it)
✗ returning the DFS result instead of the board variable
✗ in BFS: pushing None children, or measuring from root instead of node
test it yourself (paste under either solution above)
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right

t = TreeNode(1,
        TreeNode(2,
            TreeNode(4, TreeNode(9, None, TreeNode(10))),
            TreeNode(5, TreeNode(6), TreeNode(7, TreeNode(8)))),
        TreeNode(3))
s = Solution()
print(s.diameterOfBinaryTree(t))                                   # 6
print(s.diameterOfBinaryTree(TreeNode(1)))                         # 0
print(s.diameterOfBinaryTree(TreeNode(1, TreeNode(2))))            # 1
print(s.diameterOfBinaryTree(TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))))  # 3

Based on this video: Diameter of Binary Tree | BFS & DFS