DSA sheet · Trees · DFS pattern

Count Complete Tree Nodes

Counting the nodes of a tree is easy: visit each one, which is O(n). The twist here is that LeetCode asks for something faster than O(n). That is only possible because the tree is complete. The teacher first writes the simple O(n) count, then shows how the words "less than linear" point us to logarithmic → levels → height, and how the formula 2h − 1 lets us count a whole perfect subtree without visiting its nodes. The result runs in O(log² n). This is a great example of using a problem's guarantee to beat the obvious solution.

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 · Complete vs perfect binary trees

A level is one row of the tree. The height here means the number of levels (a single node has height 1, an empty tree has height 0).

perfect: every level full
        1
      /   \
     2     3
    / \   / \
   4   5 6   7
complete: last level filled from the left
        1
      /   \
     2     3
    / \   /
   4   5 6
NOT complete (gap in the last level)
        1
      /   \
     2     3
    /       \
   4         7

The teacher describes it from a node's point of view: a node has two children or none, and if it has only one, that child is the left one, never the right one.

Doubt: is "one child means a left child" the full definition?
→ It's a true property, but not the whole rule. In the third tree above, node 3 has only a right child, so the rule catches it. But look at this tree:
        1
       / \
      2   3
     / \
    4   5
   / \
  6   7
Every node has 0 or 2 children, so the one-child rule is never broken. Yet it is not complete: level 2 is not full (3 has no children), but level 3 already exists. The full test is: all levels except the last are full, and reading the last level left to right, there is no empty spot before a filled one. LeetCode promises this full version, and that's what makes the trick in Part B work.

Part A · Brute force: count every node

LeetCode 222

1The question in simple words

You get the root of a complete binary tree. Return how many nodes it has. The catch: design an algorithm that runs in less than O(n) time.

        1
      /   \
     2     3
    / \   /
   4   5 6          → answer 6

2What the constraints tell us

3Intuition

Before optimising, the teacher writes the most relaxed solution. Each node says: "me (1) + however many are on my left + however many are on my right." Ask that same question at every node, and the root ends up with the total.

4Building the logic

The recursive formula

count(node) = 1 + count(node.left) + count(node.right). The left count comes from a recursive call on root.left, the right count from a call on root.right.

What should None return?

Stand at 4, a leaf. It calls left (None) and right (None) and waits for two numbers, so that it can add 1 for itself. An empty spot has no nodes, so it must return 0. Then 4 gets 0 + 0 + 1 = 1. That's correct: the subtree at 4 has exactly one node.

Doubt 1: why must the base case return a number and not just return?
→ The parent does arithmetic with the result: 1 + left + right. Returning nothing (None) would crash the addition. 0 is the honest count of nodes in an empty spot.

Why DFS and not BFS for counting?

BFS also works: push the root, pop nodes one by one, push their children, and count the pops. Pops: 1, then 2 and 3, then 4, 5, 6 → 6 pops = 6 nodes. But the queue holds a whole level at a time, which is extra O(n) space that we don't need. So the teacher prefers DFS here.

5Approach steps

  1. If root is None → return 0.
  2. Count the left subtree recursively.
  3. Count the right subtree recursively.
  4. Return 1 + left + right.

6Code (Python)

Brute force count (O(n))
class Solution:
    def countNodes(self, root):
        if root is None:                      # empty spot: 0 nodes
            return 0
        left = self.countNodes(root.left)     # nodes on my left
        right = self.countNodes(root.right)   # nodes on my right
        return 1 + left + right               # plus me

7Code line by line

linewhat it means
if root is None: return 0Empty tree or empty spot → zero nodes. Also stops the recursion.
left = self.countNodes(root.left)Ask the left subtree how many nodes it has. Python finishes the whole left side first.
right = self.countNodes(root.right)Then ask the right subtree.
return 1 + left + rightAdd this node itself.

8Dry run using the call stack

  1. count(1) → calls count(2) first. 1 waits for its left answer, and its right call is pending.
  2. count(2) → calls count(4).
  3. count(4) → left None → 0, right None → 0 → returns 1.
  4. Back in count(2): right is count(5) → 0 + 0 + 1 = 1.
  5. count(2) = 1 + 1 + 1 = 3 → goes back as 1's left answer.
  6. count(3) → left is count(6) = 1, right is None = 0 → returns 1 + 1 + 0 = 2.
  7. count(1) = 1 + 3 + 2 = 6 ✓
step 3 (deepest)
count(1)count(2)count(4) → 1
step 6
count(1) left=3count(3)count(6) → 1

9Complexity & remember

Remember brute forceNone → 0, otherwise 1 + count(left) + count(right). It works on any tree, but it's O(n).

Part B · Optimal: use the heights

1The question in simple words

Same question. Now we really have to do less than O(n) work, which means we can't visit every node.

2What the constraints tell us

3Intuition: how the teacher thinks her way to it

  1. "Less than linear" → the next smaller usual complexity is logarithmic.
  2. In a tree, log n usually means levels. A balanced tree with n nodes has about log n levels.
  3. Levels = height. So: can we get the count from the height?
  4. Yes, for a perfect tree: a perfect tree of height h has exactly 2h − 1 nodes. So if a subtree is perfect, we know its count without visiting its nodes, just by measuring its height in O(log n).

4Building the logic from examples

Why 2h − 1?

Level 0 has 1 node, level 1 has 2, level 2 has 4, level 3 has 8… A perfect tree with h levels has 1 + 2 + 4 + … + 2h−1 nodes, and that sum is 2h − 1.

Doubt 1: where does the "− 1" come from?
→ The top level is 20 = 1 node, not 2. Each power of two is one more than the sum of all the smaller ones (8 = 4 + 2 + 1 + 1). So the sum of 1 + 2 + 4 is one short of 8.

But our tree isn't perfect

Our example (1; 2, 3; 4, 5, 6) has 3 levels, but the formula would say 7. It actually has 6, because 3's right child is missing. So first we need a quick way to tell whether a (sub)tree is perfect.

The test: walk the left edge and the right edge

In a complete tree, the last level fills from the left. So the leftmost path is the deepest and the rightmost path is the shallowest. If they are the same length, the last level reaches all the way to the right → the tree is perfect → return 2h − 1. If they differ, some nodes on the last level are missing.

Doubt 2: why is checking just the two edges enough? The middle could have a gap!
→ Not in a complete tree. A complete tree can't have a gap in the middle of the last level while its right end is filled. That's exactly why the statement says "complete". In the third picture in Part 0 (not complete), both edges have length 3, yet the tree has only 5 nodes, not 7. The trick would fail there.

When the heights differ: split

If the left and right heights are different, we can't use the formula on this node. So we do what the brute force does, just at this one node: 1 (me) + count(left) + count(right). Each child is again a complete tree, so it gets the same test.

Walking through the example

        1
      /   \
     2     3
    / \   /
   4   5 6

Why this is fast: the split only follows one path

The broken spot (where nodes are missing) is only on the last level. Upper levels are always full in a complete tree. When we split a node, the missing part is on one side, so the other child is perfect and finishes in one height check. Only one child per level keeps splitting. So we split at most about one node per level, which means about log n times.

Here is a bigger tree. The bold path is the only one that keeps splitting:

                1
            /       \
          2          3
        /   \       /   \
       4     5    6     7
      / \   / \   /
     8   9 10 11 12
nodeleft heightright heightresult
143split: 1 + count(2) + count(3)
233perfect: 2³ − 1 = 7
332split: 1 + count(6) + count(7)
621split: 1 + count(12) + count(None)
1211perfect: 1
Nonebase case0
711perfect: 1

count(6) = 1 + 1 + 0 = 2 → count(3) = 1 + 2 + 1 = 4 → count(1) = 1 + 7 + 4 = 12 ✓. Node 2's whole subtree (7 nodes) was counted with two short walks.

The 2h trick in code: left shift

1 << h means "move the bit 1 left h times", which is exactly 2h. For example, 1 << 3 = 8. So the formula becomes (1 << lh) - 1. (2 ** lh - 1 means the same thing in Python.)

5Approach steps

  1. If root is None → return 0.
  2. lh = number of nodes on the path that goes only left.
  3. rh = number of nodes on the path that goes only right.
  4. If lh == rh → perfect → return 2lh − 1.
  5. Else → return 1 + countNodes(left) + countNodes(right).

6Code (Python)

Optimal count using heights (O(log² n))
class Solution:
    def countNodes(self, root):
        if root is None:
            return 0
        lh = self.leftHeight(root)
        rh = self.rightHeight(root)
        if lh == rh:                       # perfect subtree
            return (1 << lh) - 1           # 2^h - 1, no need to visit it
        return 1 + self.countNodes(root.left) + self.countNodes(root.right)

    def leftHeight(self, root):            # walk the left edge
        height = 0
        while root:
            height += 1
            root = root.left
        return height

    def rightHeight(self, root):           # walk the right edge
        height = 0
        while root:
            height += 1
            root = root.right              # RIGHT here (see the bug below)
        return height
The bug from the videoThe teacher wrote rightHeight by copying leftHeight and forgot to change root.left to root.right. Then both functions measure the left edge, so lh == rh is always true. The code returns 23 − 1 = 7 for our 6-node tree. She spotted it when the run failed and fixed it to move right. If your answer is too big, check this first.

7Code line by line

linewhat it means
if root is None: return 0Empty (sub)tree has 0 nodes. Needed because n can be 0, and because a split can call on a missing child.
lh = self.leftHeight(root)Length of the leftmost path = the deepest path in a complete tree.
rh = self.rightHeight(root)Length of the rightmost path = the shallowest path.
if lh == rh: return (1 << lh) - 1Deepest equals shallowest → every level is full → perfect → count by formula.
return 1 + self.countNodes(root.left) + self.countNodes(root.right)Not perfect: count me, and solve both children the same way. One of them will be perfect right away.
while root: height += 1 root = root.leftCount the nodes going down only to the left until we fall off.
root = root.rightThe same walk, but only to the right.

8Dry run using the call stack

Tree 1; 2, 3; 4, 5, 6.

  1. count(1): lh = 3 (1, 2, 4), rh = 2 (1, 3). 3 ≠ 2 → split. Call count(2) first.
  2. count(2): lh = 2, rh = 2 → equal → return (1 << 2) − 1 = 3. No recursion below 2.
  3. Back in count(1): call count(3): lh = 2 (3, 6), rh = 1 (just 3) → split. Call count(6).
  4. count(6): lh = 1, rh = 1 → (1 << 1) − 1 = 1.
  5. count(None) (3's right) → 0. So count(3) = 1 + 1 + 0 = 2.
  6. count(1) = 1 + 3 + 2 = 6 ✓
step 2
count(1) splitcount(2) → 3
step 4 (deepest)
count(1) left=3count(3) splitcount(6) → 1

Compare with Part A: there, the stack went down to every leaf (4, 5, 6). Here, 4 and 5 were never visited one by one.

9Complexity & remember

Remember optimal countLeft edge height = right edge height → perfect → 2h − 1. Otherwise 1 + count(left) + count(right). Only one side keeps splitting → O(log² n).

Part C · Revision page

brute forceoptimal
ideacount every node: 1 + left + rightif the subtree is perfect, count it by formula; else split
uses "complete"?no (works on any tree)yes (edges equal ⇒ perfect only holds for complete trees)
base caseNone → 0
timeO(n)O(log² n)
spaceO(log n) stack (BFS would be O(n) queue)O(log n) stack
If you remember only 5 lines 1. "Less than O(n)" in a tree → think log n → levels → height.
2. A perfect tree with h levels has 2h − 1 nodes ((1 << h) - 1).
3. In a complete tree: leftmost path length == rightmost path length ⇒ perfect.
4. If not equal: 1 + count(left) + count(right). One child is always perfect.
5. log n splits × log n per height walk = O(log² n) time, O(log n) space.
Mistakes to avoid ✗ rightHeight moving .left (copy-paste bug, gives too big a count)
✗ forgetting the − 1 in 2h − 1
✗ measuring height in edges in one place and in nodes in another (here both count nodes)
✗ using the edge trick on a tree that isn't complete
✗ base case returning nothing instead of 0
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

six = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3, TreeNode(6)))
seven = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3, TreeNode(6), TreeNode(7)))
s = Solution()
print(s.countNodes(six))          # 6
print(s.countNodes(seven))        # 7
print(s.countNodes(TreeNode(1)))  # 1
print(s.countNodes(None))         # 0

Based on this video: Count Complete Tree Nodes | DFS