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 between1
and2^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.
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:
- Start at the root node
- If the current node is
None
, return0
- Otherwise, recursively count nodes in the left subtree by calling
countNodes(root.left)
- Recursively count nodes in the right subtree by calling
countNodes(root.right)
- 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 EvaluatorExample 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:
-
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
- Node 1 exists, so we calculate:
-
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
- Node 2 exists:
-
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
- Node 3 exists:
-
Final calculation:
- Root's count:
1 + 3 + 2 = 6
- Root's count:
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.
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
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!