Facebook Pixel

222. Count Complete Tree Nodes

Problem Description

You are given the root of a complete binary tree and need to count the total number of nodes in the tree.

A complete binary tree has special properties:

  • Every level is completely filled with nodes, except possibly the last level
  • The last level fills nodes from left to right (all nodes are as far left as possible)
  • If the tree has height h, the last level can contain between 1 and 2^h nodes

The key constraint is that your algorithm must run in less than O(n) time complexity, where n is the number of nodes. This means you cannot simply visit every node to count them.

The provided solution uses a recursive approach that visits each node exactly once, counting 1 for the current node plus the counts from the left and right subtrees. For each node visited, it adds 1 to the count and recursively counts nodes in both subtrees. The base case returns 0 when encountering a None node.

While this recursive solution correctly counts all nodes, it has O(n) time complexity since it visits every node. To achieve better than O(n) complexity as required, you would need to leverage the complete binary tree properties to avoid visiting all nodes.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When counting nodes in any binary tree, the most straightforward approach is to visit each node and count it. We know that for any node, the total count equals 1 (for the current node) plus the count of nodes in its left subtree plus the count of nodes in its right subtree. This naturally leads to a recursive solution.

The recursive pattern emerges from breaking down the problem: if we can count nodes in smaller subtrees, we can build up to count the entire tree. At each node, we ask: "How many nodes are in my left subtree? How many in my right subtree?" Then we add these counts plus 1 for ourselves.

The base case is clear - when we reach a None node (beyond the leaves), there are 0 nodes to count. This stops our recursion from going infinitely.

While the problem mentions that this is a complete binary tree and asks for better than O(n) complexity, the provided solution takes the direct approach of counting every node. The complete binary tree property could be exploited for optimization - for instance, if we know the leftmost and rightmost depths are equal, the tree is perfect and we can calculate the count as 2^height - 1 without visiting all nodes. However, the given solution prioritizes simplicity and correctness over this optimization, treating it like any binary tree and ensuring every node is counted exactly once through recursive traversal.

Solution Approach

The solution uses a recursive depth-first search (DFS) approach to count nodes in the binary tree. Here's how the implementation works:

Base Case: When we encounter a None node (empty subtree), we return 0 since there are no nodes to count.

if root is None:
    return 0

Recursive Case: For any non-null node, we calculate the total count using the formula:

  • Count = 1 (current node) + nodes in left subtree + nodes in right subtree
return 1 + self.countNodes(root.left) + self.countNodes(root.right)

The algorithm follows these steps:

  1. Start at the root node
  2. If the current node is None, return 0
  3. Otherwise, recursively count nodes in the left subtree by calling countNodes(root.left)
  4. Recursively count nodes in the right subtree by calling countNodes(root.right)
  5. Add 1 for the current node and return the sum

Example walkthrough: For a tree with 3 nodes (root with two children):

  • countNodes(root)1 + countNodes(left) + countNodes(right)
  • countNodes(left)1 + countNodes(None) + countNodes(None) = 1 + 0 + 0 = 1
  • countNodes(right)1 + countNodes(None) + countNodes(None) = 1 + 0 + 0 = 1
  • Final result: 1 + 1 + 1 = 3

Time Complexity: O(n) where n is the number of nodes, as we visit each node exactly once.

Space Complexity: O(h) where h is the height of the tree, due to the recursive call stack. In the worst case of a skewed tree, this could be O(n).

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through counting nodes in a small complete binary tree with 6 nodes:

        1
       / \
      2   3
     / \ /
    4  5 6

Step-by-step execution:

  1. Start at root (1):

    • Node 1 exists, so we calculate: 1 + countNodes(left:2) + countNodes(right:3)
    • We need to find the counts for nodes 2 and 3
  2. Process left subtree (node 2):

    • Node 2 exists: 1 + countNodes(left:4) + countNodes(right:5)
    • Count node 4: 1 + countNodes(None) + countNodes(None) = 1 + 0 + 0 = 1
    • Count node 5: 1 + countNodes(None) + countNodes(None) = 1 + 0 + 0 = 1
    • Node 2's total: 1 + 1 + 1 = 3
  3. Process right subtree (node 3):

    • Node 3 exists: 1 + countNodes(left:6) + countNodes(right:None)
    • Count node 6: 1 + countNodes(None) + countNodes(None) = 1 + 0 + 0 = 1
    • Count right (None): Returns 0
    • Node 3's total: 1 + 1 + 0 = 2
  4. Final calculation:

    • Root's count: 1 + 3 + 2 = 6

The recursion builds up from the leaves, where each leaf node counts as 1 (itself + 0 from null children), then parent nodes sum their children's counts plus 1 for themselves, ultimately giving us the total count of 6 nodes.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8from typing import Optional
9
10class Solution:
11    def countNodes(self, root: Optional[TreeNode]) -> int:
12        """
13        Count the total number of nodes in a binary tree.
14      
15        Args:
16            root: The root node of the binary tree.
17          
18        Returns:
19            The total count of nodes in the tree.
20        """
21        # Base case: if the current node is None, return 0
22        if root is None:
23            return 0
24      
25        # Recursive case: count current node (1) plus all nodes in left and right subtrees
26        # The total count = 1 (current node) + nodes in left subtree + nodes in right subtree
27        return 1 + self.countNodes(root.left) + self.countNodes(root.right)
28
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    /**
18     * Counts the total number of nodes in a binary tree.
19     * Uses recursive approach to traverse the entire tree.
20     * 
21     * @param root The root node of the binary tree
22     * @return The total count of nodes in the tree
23     */
24    public int countNodes(TreeNode root) {
25        // Base case: if the current node is null, return 0
26        if (root == null) {
27            return 0;
28        }
29      
30        // Recursive case: count current node (1) plus all nodes in left and right subtrees
31        // The recursion will traverse every node in the tree exactly once
32        return 1 + countNodes(root.left) + countNodes(root.right);
33    }
34}
35
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    /**
15     * Counts the total number of nodes in a binary tree
16     * @param root - pointer to the root node of the binary tree
17     * @return total count of nodes in the tree
18     */
19    int countNodes(TreeNode* root) {
20        // Base case: if the current node is null, return 0
21        if (!root) {
22            return 0;
23        }
24      
25        // Recursive case: count current node (1) plus all nodes in left and right subtrees
26        return 1 + countNodes(root->left) + countNodes(root->right);
27    }
28};
29
1/**
2 * Definition for a binary tree node
3 */
4interface TreeNode {
5    val: number;
6    left: TreeNode | null;
7    right: TreeNode | null;
8}
9
10/**
11 * Counts the total number of nodes in a binary tree
12 * @param root - The root node of the binary tree
13 * @returns The total count of nodes in the tree
14 */
15function countNodes(root: TreeNode | null): number {
16    // Base case: if the node is null, return 0
17    if (!root) {
18        return 0;
19    }
20  
21    // Recursive case: count current node (1) plus all nodes in left and right subtrees
22    return 1 + countNodes(root.left) + countNodes(root.right);
23}
24

Time and Space Complexity

Time Complexity: O(n), where n is the number of nodes in the tree.

The algorithm visits every node in the binary tree exactly once. For each node, it performs a constant amount of work (returning 1 plus the recursive calls). Since we traverse all n nodes, the total time complexity is O(n).

Space Complexity: O(n), where n is the number of nodes in the tree.

The space complexity is determined by the recursive call stack. In the worst case, when the tree is completely unbalanced (essentially a linked list), the recursion depth can reach n levels deep, requiring O(n) space on the call stack. In the best case of a perfectly balanced tree, the recursion depth would be O(log n), but we consider the worst-case scenario for space complexity analysis, which gives us O(n).

Common Pitfalls

Pitfall 1: Not Leveraging Complete Binary Tree Properties

The most significant pitfall in the provided solution is that it doesn't take advantage of the complete binary tree properties to achieve better than O(n) time complexity. The current solution visits every single node, which violates the problem's requirement of running in less than O(n) time.

Why it's a problem: For large complete binary trees, visiting all n nodes becomes inefficient when we could use mathematical properties to calculate the count faster.

Solution: Use the complete binary tree properties to optimize:

class Solution:
    def countNodes(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
      
        # Calculate left and right heights
        left_height = self.get_left_height(root)
        right_height = self.get_right_height(root)
      
        # If heights are equal, tree is perfect
        if left_height == right_height:
            return (2 ** left_height) - 1
      
        # Otherwise, recursively count
        return 1 + self.countNodes(root.left) + self.countNodes(root.right)
  
    def get_left_height(self, node):
        height = 0
        while node:
            height += 1
            node = node.left
        return height
  
    def get_right_height(self, node):
        height = 0
        while node:
            height += 1
            node = node.right
        return height

This optimized approach runs in O(log² n) time complexity by checking if subtrees are perfect binary trees.

Pitfall 2: Stack Overflow for Very Deep Trees

While less common for complete binary trees, the recursive approach can cause stack overflow for trees with extreme depth.

Why it's a problem: Python has a default recursion limit (usually around 1000), and very deep trees could exceed this limit.

Solution: Use an iterative approach with explicit stack:

class Solution:
    def countNodes(self, root: Optional[TreeNode]) -> int:
        if not root:
            return 0
      
        count = 0
        stack = [root]
      
        while stack:
            node = stack.pop()
            count += 1
          
            if node.right:
                stack.append(node.right)
            if node.left:
                stack.append(node.left)
      
        return count

Pitfall 3: Misunderstanding Complete Binary Tree Definition

Developers might incorrectly assume all levels are fully filled or implement unnecessary validation for tree completeness.

Why it's a problem: Adding unnecessary checks for tree completeness wastes computational resources when the problem guarantees the input is a complete binary tree.

Solution: Trust the problem constraints and focus on leveraging the complete tree properties rather than validating them. The optimal solution should use binary search on the last level to find the exact number of nodes in O(log² n) time.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More