958. Check Completeness of a Binary Tree
Problem Description
In this problem, we are provided with the root node of a binary tree. Our objective is to determine whether the binary tree is a "complete binary tree" or not. A complete binary tree has a few defining characteristics:
- All levels are completely filled, except possibly the last level.
- On the last level, if it’s not completely filled, all the nodes are as far left as possible.
This definition means that in a complete binary tree, if we traverse the nodes level by level from left to right, we will encounter all the non-null nodes first before any null node. If any null nodes are found, there should not be any more non-null nodes after them in this level order traversal.
To sum it up, the task is to check if the given binary tree meets the complete binary tree criteria.
Flowchart Walkthrough
Let's analyze Leetcode problem 958, "Check Completeness of a Binary Tree," using the Flowchart. Here’s a detailed step-by-step walkthrough based on the given flowchart:
Is it a graph?
- Yes: Even though it's specifically a binary tree, a binary tree is a type of graph with nodes connected by edges.
Is it a tree?
- Yes: A binary tree is a specialized form of a tree with nodes having up to two children.
Is the problem related to directed acyclic graphs (DAGs)?
- No: While a binary tree is a DAG by definition (since all trees are DAGs), the nature of the problem — checking the completeness of the binary tree — does not relate to typical DAG-specific problems like topological sorting.
Is the problem related to shortest paths?
- No: The problem involves checking the structural properties of the tree (its completeness), not finding paths or distances within it.
Does the problem involve connectivity?
- No: We are not checking for connectivity between various nodes beyond the inherent connections of the tree structure itself. Instead, we are verifying if every level, except possibly the last, is fully filled, and all nodes in the last level are as left as possible.
Conclusion: Following down the flowchart, the next logical algorithm would normally be DFS. However, to directly determine the completeness of a binary tree, the BFS approach is useful as it processes nodes level by level. BFS can help by ensuring all levels are filled completely and correctly.
Thus, while the flowchart leads most graph-related problems through BFS when it comes to tree structure checks, Breadth-First Search can be chosen here for its level-by-level checking capabilities, which isn't explicitly detailed in the flowchart for this specific problem type.
Intuition
To determine if a binary tree is complete, we can perform a level order traversal on the tree. In an ordinary level order traversal (also known as breadth-first search), we enqueue all children of each node and process each node from the left to the right of each level.
The solution is based on an observation that in a complete binary tree, once we encounter the first null node (representing a missing child in the last level or an indication of no more nodes on the last level), all subsequent nodes encountered during the level order traversal must also be null. If we find a non-null node after encountering a null, the tree cannot be considered complete.
Here's the step-by-step reasoning for arriving at the solution:
- Initiate a queue to perform level order traversal. Start by enqueuing the root node.
- Begin the level order traversal, dequeuing a node from the front of the queue at each step.
- Check the current node:
- If it's not null, enqueue its left and right children (regardless of whether they're null or not) for later processing.
- If the node is null, this indicates the end of all the non-null nodes encountered so far for the complete binary tree. Any subsequent nodes should be null to ensure the tree is complete.
- After encountering a null node, check the rest of the queue:
- If all remaining nodes in the queue are null, the tree is complete.
- If any non-null node is found, then it's not a complete binary tree.
The algorithm will employ a deque (short for double-ended queue) as it allows efficient insertions and deletions from both ends, which is perfect for our level order traversal needs.
Learn more about Tree, Breadth-First Search and Binary Tree patterns.
Solution Approach
The solution approach implements a level order traversal of the binary tree using a queue data structure. The standard queue is replaced with a deque
from Python's collections module for efficient manipulation of the queue's endpoints.
Here's a step-by-step overview of the implementation:
-
Initialize a
deque
and add the root node of the binary tree to it.q = deque([root])
-
Begin iterating through the queue:
- For each iteration, dequeue (
popleft
) an element from the front of the queue. - This element represents the current node being processed.
while q: node = q.popleft()
- For each iteration, dequeue (
-
The key condition: If the current node is
None
, it signals that we've encountered a gap in our level order traversal. Break out of the loop as we should not find any more non-null nodes after this point.if node is None: break
-
If the current node is not
None
, enqueue its children (left and right) to the back of the queue. These operations will includeNone
values if the current node doesn't have a corresponding child.q.append(node.left) q.append(node.right)
-
After breaking out of the loop, we must verify that all the remaining nodes in the queue are
None
. This would confirm that we did not encounter any non-null nodes after the firstNone
node, which is a requirement for a complete binary tree.return all(node is None for node in q)
This method relies heavily on the properties of a complete binary tree observed during a level order traversal. By utilizing these properties, the algorithm effectively determines the completeness of the binary tree with an efficient traversal and a simple while loop.
The pattern used in this method is a common Breadth-First Search (BFS) pattern, adapted to check the continuity of node presence at each level of the tree. The presence of a None
value in the queue is used as an indicator to signal potential discontinuity, which would imply the binary tree isn't complete. After the first None
is seen, the result of the all
function confirms whether the rest of the elements left in the queue adhere to the complete binary tree property.
Implementing this approach provides an intuitive and direct method to verify that a binary tree is a complete binary tree. The use of a queue to keep track of nodes for processing and the break condition for encountering None
values combine to form an elegant solution for this problem.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's assume we have a binary tree structured like this:
1 / \ 2 3 / \ / 4 5 6
This binary tree is a complete binary tree because all levels except possibly the last one are fully filled, and all nodes in the last level are as far left as possible.
Now let's walk through the solution approach with this binary tree as an example:
-
We initialize a
deque
and add the root node of the binary tree (the node with value 1) to it.q = deque([1])
-
We begin iterating through the queue.
- We dequeue an element from the front of the queue (
popleft()
), which is the root node with value 1. - Since it is not
None
, we enqueue its children nodes with values 2 and 3.
while q: node = q.popleft() # node with value 1 q.append(node.left) # node with value 2 q.append(node.right) # node with value 3
- We dequeue an element from the front of the queue (
-
Since node with value 1 is not
None
, we continue with the loop:- We again
popleft()
on the deque, this time getting the node with value 2. - We enqueue its children nodes with values 4 and 5.
node = q.popleft() # node with value 2 q.append(node.left) # node with value 4 q.append(node.right) # node with value 5
- We again
-
We repeat the above step for node with value 3, enqueuing its child with value 6. Since node 3 has no right child, we insert
None
.node = q.popleft() # node with value 3 q.append(node.left) # node with value 6 q.append(node.right) # None
-
Next, we process nodes 4, 5, and 6. Since they are at the last level and have no children, they would add
None
values to the queue.- Eventually, we encounter the first
None
value after all nodes on the last level.
node = q.popleft() # node with value 4, adds None values to the queue node = q.popleft() # node with value 5, adds None values to the queue node = q.popleft() # node with value 6, adds None values to the queue node = q.popleft() # None, indicates end of non-null nodes
- Eventually, we encounter the first
-
Since we encountered a
None
, we break out of the loop. -
Now we check that the rest of the queue only contains
None
elements. If it does, we confirm that we have a complete binary tree. In this case, all remaining elements areNone
, so the tree is complete.return all(node is None for node in q) # returns True, confirming the tree is complete
By following this level order traversal and checking for the first occurrence of None
, we have verified the binary tree is complete using our example.
Solution Implementation
1from collections import deque # Import deque, as it's used for the queue.
2
3# Definition for a binary tree node.
4class TreeNode:
5 def __init__(self, value=0, left=None, right=None):
6 self.value = value
7 self.left = left
8 self.right = right
9
10class Solution:
11 def isCompleteTree(self, root: TreeNode) -> bool:
12 # Initialize a queue with the root node to check level by level.
13 nodes_queue = deque([root])
14
15 # Iterate over the nodes until you find the first None, which signals incomplete level.
16 while nodes_queue:
17 current_node = nodes_queue.popleft()
18
19 # When a None is encountered, the check for completeness starts.
20 if current_node is None:
21 break
22
23 # Append left and right children to the queue for further level checking.
24 nodes_queue.append(current_node.left)
25 nodes_queue.append(current_node.right)
26
27 # At this point, all remaining nodes in the queue should be None for a complete tree.
28 # This loop checks if there is any node that is not None after the first None was found.
29 for node in nodes_queue:
30 if node is not None:
31 return False # The tree is not complete as there is a node after a None.
32
33 return True # The tree is complete as all nodes after the first None are None.
34
35# Example usage:
36# tree = TreeNode(1, TreeNode(2), TreeNode(3))
37# solution = Solution()
38# print(solution.isCompleteTree(tree)) # Output: True
39
1/**
2 * Definition for a binary tree node.
3 */
4class TreeNode {
5 int val;
6 TreeNode left;
7 TreeNode right;
8
9 TreeNode() {}
10
11 TreeNode(int val) {
12 this.val = val;
13 }
14
15 TreeNode(int val, TreeNode left, TreeNode right) {
16 this.val = val;
17 this.left = left;
18 this.right = right;
19 }
20}
21
22class Solution {
23 /**
24 * Determine if a binary tree is complete.
25 * A binary tree is complete if all levels are completely filled except possibly the last,
26 * which is filled from left to right.
27 *
28 * @param root The root node of the binary tree.
29 * @return true if the tree is complete, false otherwise.
30 */
31 public boolean isCompleteTree(TreeNode root) {
32 // Queue to hold tree nodes for level order traversal
33 Deque<TreeNode> queue = new LinkedList<>();
34 queue.offer(root);
35
36 // Traverse the tree using breadth-first search.
37 // Keep adding children nodes until a null is encountered.
38 while (queue.peek() != null) {
39 TreeNode currentNode = queue.poll();
40 queue.offer(currentNode.left);
41 queue.offer(currentNode.right);
42 }
43
44 // Remove any trailing nulls from the queue.
45 // If any non-null nodes are found afterwards it means that the tree is not complete.
46 while (!queue.isEmpty() && queue.peek() == null) {
47 queue.poll();
48 }
49
50 // If the queue is empty, then all the levels of the tree are fully filled.
51 // Otherwise, the tree is not complete.
52 return queue.isEmpty();
53 }
54}
55
1#include <queue>
2
3// Definition for a binary tree node.
4struct TreeNode {
5 int val;
6 TreeNode *left;
7 TreeNode *right;
8 // Constructor initializes the node with a value, and the left and right pointers as null.
9 TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
10 TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
11};
12
13class Solution {
14public:
15 // Method to check if a binary tree is complete.
16 bool isCompleteTree(TreeNode* root) {
17 // We use a queue to do a level order traversal of the tree.
18 std::queue<TreeNode*> nodeQueue;
19 nodeQueue.push(root);
20
21 // Continue level order traversal until we find a null pointer.
22 while (nodeQueue.front() != nullptr) {
23 TreeNode* currentNode = nodeQueue.front();
24 nodeQueue.pop(); // Remove front of the queue after saving the node.
25
26 // Add child nodes to the queue for further processing.
27 nodeQueue.push(currentNode->left);
28 nodeQueue.push(currentNode->right);
29 }
30
31 // After finding the first null pointer, all subsequent nodes must also be null.
32 // Pop all null pointers from the queue.
33 while (!nodeQueue.empty() && nodeQueue.front() == nullptr) {
34 nodeQueue.pop();
35 }
36
37 // If the queue is empty, it means there were no non-null nodes after the first null
38 // which indicates the tree is complete. Otherwise, the tree is not complete.
39 return nodeQueue.empty();
40 }
41};
42
1// Definition for a binary tree node.
2class TreeNode {
3 val: number;
4 left: TreeNode | null;
5 right: TreeNode | null;
6
7 // Constructor initializes the node with a value, and the left and right pointers as null.
8 constructor(val: number, left?: TreeNode | null, right?: TreeNode | null) {
9 this.val = val;
10 this.left = left === undefined ? null : left;
11 this.right = right === undefined ? null : right;
12 }
13}
14
15// Method to check if a binary tree is complete.
16function isCompleteTree(root: TreeNode | null): boolean {
17 if (!root) {
18 // If the root is null, the tree is trivially complete.
19 return true;
20 }
21
22 // We use a queue to do a level order traversal of the tree.
23 const nodeQueue: (TreeNode | null)[] = [root];
24
25 let foundNull = false;
26
27 // Continue level order traversal until the queue is empty.
28 while (nodeQueue.length > 0) {
29 const currentNode = nodeQueue.shift();
30
31 if (!currentNode) {
32 // We encountered a null node; from this point forward, all nodes must be null.
33 foundNull = true;
34 } else {
35 if (foundNull) {
36 // If we previously encountered a null node and now we see a non-null node,
37 // then the tree is not complete.
38 return false;
39 }
40 // Add child nodes to the queue for further processing.
41 nodeQueue.push(currentNode.left);
42 nodeQueue.push(currentNode.right);
43 }
44 }
45
46 // If we finished processing all nodes without finding a non-null
47 // node after a null node, the tree is complete.
48 return true;
49}
50
Time and Space Complexity
The time complexity of the given code is O(n)
, where n
is the number of nodes in the binary tree. This is because each node is processed exactly once when it is dequeued for examination.
The space complexity of the code is also O(n)
. In the worst-case scenario, the queue could contain all the nodes at the last level of a complete binary tree, just before the loop terminates upon encountering a None
node. This means that the number of elements in the queue could approach the number of leaves in the full binary tree, which is approximately n/2
. Considering the constant factor is omitted in Big O notation, the space complexity remains O(n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
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
https algomonster s3 us east 2 amazonaws com cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https algomonster s3 us east 2 amazonaws com 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!