DSA sheet · Trees · DFS pattern

Balanced Binary Tree

This problem is really the definition of a balanced tree turned into code. The teacher first solves it with BFS (measure the heights again for every node, O(n²)), then with DFS (get the heights from below in one pass, O(n)). On the way she writes a DFS that looks right but gives a wrong answer, shows the example that breaks it, and fixes it. That fix, "once a subtree says −1, pass −1 straight up", is the main lesson. The DFS is almost the same as Diameter of Binary Tree, so read that first if you haven't.

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
heightThe number of levels in a tree (nodes on its longest top-to-bottom path). Empty tree → 0, single node → 1.
left height / right height of a nodeThe height of its left subtree and of its right subtree.
absolute differenceThe gap between two numbers, ignoring which one is bigger: abs(3 - 1) = abs(1 - 3) = 2.
height-balancedAt every node, the left height and the right height differ by at most 1. A gap of 0 or 1 is fine. A gap of 2 or more anywhere makes the whole tree not balanced.
skewed treeA tree where every node has only one child, so it looks like a straight line. Its height is n.

Part A · Balanced check with BFS (brute force)

LeetCode 110

1The question in simple words

Given the root of a binary tree, return True if it is height-balanced, otherwise False.

Example 1: balanced ✓
      1
     / \
    2   3
       / \
      4   5
Example 2: not balanced ✗
      1
     / \
    2   6
     \
      3
       \
        4
         \
          5
Doubt: is it enough to check only the root?
→ No. In Example 2 the root has left height 4 (2, 3, 4, 5) and right height 1: gap 3, so the root fails here. But you can build trees where the root's two sides are equal and some lower node is lopsided, like the one below: the root sees 3 vs 3, yet node 2 sees 2 vs 0. The definition says every node, so every node must be checked.
        1
       / \
      2   5
     /   /
    3   6
   /   /
  4   7

2What the constraints tell us

3Intuition

Stand on a node. To judge it, you need two numbers: how tall is my left side, how tall is my right side. If the gap is more than 1, stop and say False. Do this for every node. If nobody fails, say True.

The BFS way: visit nodes level by level with a queue. For each one, call a helper that measures height by counting levels (a level-order traversal, the same helper as in Diameter).

4Building the logic

Doubt 1: why abs()?
→ Either side can be the taller one. Without abs, a taller right side gives a negative number, and a negative number is never > 1, so we'd miss it.
Doubt 2: why push only children that exist?
→ Every popped node gets .left and .right read from it. Popping a None would crash.

5Approach steps

  1. If root is None → return True.
  2. Put the root in a queue.
  3. While the queue isn't empty: pop a node, measure the left and right heights with a level-order count.
  4. If the gap is more than 1 → return False.
  5. Push the existing children.
  6. After the loop → return True.

6Code (Python)

Balanced check with BFS (brute force)
from collections import deque

class Solution:
    def isBalanced(self, root):
        if root is None:                       # 0 nodes is allowed
            return True
        queue = deque([root])
        while queue:
            node = queue.popleft()
            left = self.height(node.left)
            right = self.height(node.right)
            if abs(left - right) > 1:          # this node is lopsided
                return False
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        return True                            # nobody failed

    def height(self, root):                    # count levels, store nothing
        if root is None:
            return 0
        queue = deque([root])
        height = 0
        while queue:
            for _ in range(len(queue)):        # one full level
                node = queue.popleft()
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            height += 1
        return height

7Code line by line

linewhat it means
if root is None: return TrueAn empty tree has no lopsided node, so it is balanced. Needed because n can be 0.
node = queue.popleft()Next node to judge.
left = self.height(node.left) right = self.height(node.right)Measure both sides from scratch with a level count.
if abs(left - right) > 1: return FalseGap of 2 or more → the whole tree fails. Stop now.
queue.append(...)Queue the real children so they are judged too.
return TrueWe only get here if every node passed.
for _ in range(len(queue))In the helper: pop exactly one level. range(len(queue)) is computed once, before the loop starts, so the newly pushed children wait for the next round.
height += 1One more level counted.

8Dry run: Example 1

start1
pop 1height(2) = 1, height(3) = 2 → gap 1 ✓ → push 2, 3
23
pop 20 vs 0 → gap 0 ✓ → no children
pop 31 vs 1 → gap 0 ✓ → push 4, 5
45
pop 4, 50 vs 0 each ✓
endqueue empty → return True ✓

On Example 2: pop 1 → height(2) = 4, height(6) = 1 → gap 3 → False straight away.

9Complexity & remember

Remember the BFS ideaEvery node: measure both heights from scratch, fail on a gap > 1. Correct, but the re-measuring makes it O(n²).

Part B · DFS, first try (has a bug)

1The question (same as Part A)

Same check, same examples. We want one pass instead of re-measuring.

2Constraints

Same as Part A. The base case (None) is a must in DFS for two reasons, as the teacher stresses: the tree itself can be empty, and the recursion will always reach a None child at the bottom.

3Intuition

A node can't be judged until it knows both heights, so go deep first and decide on the way back up (left, right, then the node). Each child returns its height to its parent, exactly like in Diameter. So we never re-measure a subtree.

The new problem: how does a node say "I failed"?

The function returns a height, which is a number. When a node finds a gap > 1, it wants to say "False", but the parent is waiting for a number to compare. So we need a number that means False.

Heights are always 0, 1, 2, … (never negative). So any negative number can't be confused with a real height. The teacher picks −1 to mean "not balanced". (−2 or any negative would also work.) At the very end, the main function turns it back into a boolean: height(root) != -1.

Doubt: Python lets a function return either an int or a bool. Why not just return False?
→ In Python False == 0, and max(False, 2) is 2. A returned False would silently act like height 0 and the failure would be lost. A clear −1 is safer and also matches Java/C++.

4Building the logic

5Approach steps

  1. isBalanced(root) returns height(root) != -1.
  2. In height(node): None → 0.
  3. Get left and right heights.
  4. Gap > 1 → −1. Else → max(left, right) + 1.

6Code (Python), the buggy version

DFS first try: WRONG on some trees
class Solution:
    def isBalanced(self, root):
        return self.height(root) != -1

    def height(self, root):
        if root is None:
            return 0
        left = self.height(root.left)
        right = self.height(root.right)
        if abs(left - right) > 1:
            return -1                      # "not balanced"
        return max(left, right) + 1

7Code line by line

linewhat it means
return self.height(root) != -1−1 means "failed somewhere". Anything else is a real height, so the tree is balanced.
if root is None: return 0Empty spot has height 0. Stops the recursion.
left = ..., right = ...Heights of the two sides, collected from below.
if abs(left - right) > 1: return -1This node is lopsided → send the "False" signal up.
return max(left, right) + 1This node is fine → send its height up.

8Dry run: it works on Example 1, fails on Example 2

Example 1 (works)

  1. h(1) → h(2) → h(None) = 0 and h(None) = 0. Gap 0 → h(2) returns 1.
  2. h(1) goes right: h(3) → h(4): leaf → 1. h(5): leaf → 1.
  3. h(3): 1 vs 1 → gap 0 → returns 1 + 1 = 2.
  4. h(1): 1 vs 2 → gap 1, fine → returns 2 + 1 = 3.
  5. 3 != −1 → True ✓
going into 4
h(1)h(3)h(4) → 1
3 returns
h(1)h(3) → 2

Example 2 (fails!)

  1. Calls go 1 → 2 → (2 has no left: 0) → 3 → (3 has no left: 0) → 4 → (4 has no left: 0) → 5.
  2. h(5): leaf → 1.
  3. h(4): left 0, right 1 → gap 1, fine → returns 2.
  4. h(3): left 0, right 2 → gap 2 → returns −1. Good, it noticed.
  5. h(2): left 0, right −1. Gap = abs(0 − (−1)) = 1 → "fine"?! Returns max(0, −1) + 1 = 1. The −1 has disappeared.
  6. h(1): left 1, right h(6) = 1 → gap 0 → returns 2.
  7. 2 != −1 → True ✗. Expected False.
What went wrongThe −1 is a signal, not a height. But node 2 treated it as a height: it put it into abs() (where 0 and −1 look only 1 apart) and into max() (where −1 loses to 0). So the failure got swallowed.

9Complexity & remember

O(n) time, but wrong, so it doesn't matter yet. Never mix a signal value with real values without checking it first.


Part C · DFS, fixed (optimal)

1The question and 2 constraints

Same as before. Nothing changes here.

3Intuition: what changes from Part B

If a child already said "not balanced", the whole tree is not balanced. There's nothing left to measure. So the moment a call gets −1 from a child, it returns −1 at once, before doing any abs() or max(). The −1 then travels straight up to the top.

4Building the logic: two new lines

Doubt: does the order of these lines matter?
→ Yes. The −1 checks must come before the abs() check, because that's exactly where Part B broke. Putting the left check before the right call is a bonus: it skips the whole right subtree once the left has failed.

5Approach steps

  1. None → 0.
  2. left = height(left child). If −1 → return −1.
  3. right = height(right child). If −1 → return −1.
  4. If abs(left − right) > 1 → return −1.
  5. Return max(left, right) + 1.
  6. Main: return height(root) != -1.

6Code (Python)

Balanced check with DFS (optimal)
class Solution:
    def isBalanced(self, root):
        return self.height(root) != -1

    def height(self, root):
        if root is None:                   # empty tree / bottom of recursion
            return 0
        left = self.height(root.left)
        if left == -1:                     # left already failed: stop
            return -1
        right = self.height(root.right)
        if right == -1:                    # right already failed: stop
            return -1
        if abs(left - right) > 1:          # this node is lopsided
            return -1
        return max(left, right) + 1        # real height for the parent

7Code line by line (only the new lines)

linewhat it means
if left == -1: return -1Somewhere in the left subtree a node failed. Pass the signal up and don't even visit the right side.
if right == -1: return -1Same for the right subtree.
if abs(left - right) > 1Now both are true heights, so the gap is a real gap.

8Dry run: Example 2 again

  1. h(5) → 1. h(4): left 0, right 1 → returns 2.
  2. h(3): left 0, right 2 → gap 2 → −1.
  3. h(2): left 0 (fine). Right = −1 → return −1 immediately (no abs check).
  4. h(1): left = −1 → return −1 immediately. h(6) is never called, the right side is skipped.
  5. Main: −1 != −1 is False → False ✓
deepest point
h(1)h(2)h(3)h(4)h(5) → 1
3 fails
h(1)h(2)h(3) → −1
−1 rises
h(1) → −1

Example 1 runs exactly as in Part B (no −1 ever appears) and returns True.

9Complexity & remember

Remember Balanced (DFS)None → 0 · left −1? return −1 · right −1? return −1 · gap > 1? return −1 · else max + 1. Answer = height(root) != -1.

Part D · Revision page

BFS (A)DFS first try (B)DFS fixed (C)
heights come froma fresh level count per nodechildren's return valueschildren's return values
"not balanced" shown byreturn False−1−1
checks −1 before abs/max?n/anoyes
correct?yesno (Example 2)yes
time / spaceO(n²) / O(2n)O(n) / O(n)O(n) / O(n)
DiameterBalanced
base caseNone → 0
at each nodeupdate board with L + Rfail (−1) if |L − R| > 1
returnedmax(L, R) + 1
If you remember only 5 lines 1. Balanced = at every node, |left height − right height| ≤ 1.
2. BFS re-measures heights for each node → O(n²).
3. DFS returns heights from below → O(n). Base: None → 0.
4. The function returns a number, so "False" is sent as −1 (heights are never negative).
5. Check for −1 before abs/max and return it at once.
Mistakes to avoid ✗ checking only the root
✗ forgetting abs()
✗ forgetting the empty-tree base case in BFS (n can be 0)
✗ letting −1 go into abs()/max() (the Part B bug)
✗ returning False from the height function in Python (False acts like 0)
✗ saying space is O(log n): the tree might be skewed
test it yourself (paste under any solution above)
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right

ex1 = TreeNode(1, TreeNode(2), TreeNode(3, TreeNode(4), TreeNode(5)))
ex2 = TreeNode(1, TreeNode(2, None, TreeNode(3, None, TreeNode(4, None, TreeNode(5)))), TreeNode(6))
s = Solution()
print(s.isBalanced(ex1))     # True
print(s.isBalanced(ex2))     # False  (Part B's buggy code prints True here)
print(s.isBalanced(None))    # True
print(s.isBalanced(TreeNode(1, TreeNode(2, TreeNode(3)))))   # False

Based on this video: Balanced Binary Tree | BFS & DFS