919. Complete Binary Tree Inserter
Problem Description
A complete binary tree is a binary tree where every level is completely filled, except possibly the last level. In the last level, all nodes are positioned as far left as possible.
You need to design a data structure that maintains a complete binary tree and supports insertion operations while keeping the tree complete.
Implement the CBTInserter
class with the following methods:
CBTInserter(TreeNode root)
: Initialize the data structure with an existing complete binary tree rooted atroot
.int insert(int v)
: Insert a new node with valuev
into the tree while maintaining its completeness property. Return the value of the parent node of the newly inserted node.TreeNode get_root()
: Return the root node of the tree.
The key challenge is to efficiently determine where to insert the next node to maintain the complete binary tree property. In a complete binary tree, nodes are filled level by level from left to right.
The solution uses an array-based representation where all nodes are stored in level-order (BFS) traversal order. This allows for easy parent-child relationship calculations:
- For a node at index
i
, its parent is at index(i-1)//2
- For a parent at index
i
, its left child would be at index2*i+1
and right child at2*i+2
During initialization, the existing tree is traversed using BFS and all nodes are stored in the array. When inserting a new node, the parent can be found at index (len(tree)-1)//2
, and the new node is added as either the left or right child depending on which position is empty.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: A tree is a special type of graph (acyclic connected graph). The problem explicitly deals with a binary tree structure.
Is it a tree?
- Yes: The problem specifically mentions we're working with a complete binary tree.
DFS?
- Not necessarily: While DFS could work, for a complete binary tree where we need to maintain level-order insertion (filling levels from left to right), BFS is more natural. Let's reconsider...
Actually, looking at the solution approach, it uses BFS (level-order traversal) to initialize the tree structure. The queue-based traversal in the initialization clearly indicates BFS is being used:
q = deque([root])
while q:
for _ in range(len(q)):
node = q.popleft()
self.tree.append(node)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
The BFS pattern is ideal here because:
- Level-order traversal: BFS naturally processes nodes level by level, which aligns perfectly with how a complete binary tree is structured
- Array representation: By traversing in BFS order, nodes are stored in the array following the heap property where parent-child relationships can be calculated using indices
- Efficient insertion: The array representation obtained from BFS allows O(1) parent finding for new insertions
Conclusion: The flowchart leads us to BFS through the tree path, as BFS is the optimal approach for maintaining and manipulating a complete binary tree structure where level-order properties are crucial.
Intuition
The key insight comes from understanding the properties of a complete binary tree. In a complete binary tree, nodes are filled level by level from left to right, which creates a very predictable pattern.
Think about how nodes are numbered in a complete binary tree if we label them in level-order (BFS) sequence starting from 0:
- Root is at index 0
- For any node at index
i
, its left child is at index2*i + 1
and right child is at2*i + 2
- For any node at index
i
, its parent is at index(i-1) // 2
This mathematical relationship is exactly like a heap data structure! This means we can represent the entire tree as an array where the position in the array determines the parent-child relationships.
When we need to insert a new node while maintaining completeness, we know it must go at the next available position in level-order. If we maintain all nodes in an array following BFS order, the next insertion position is simply the next index in the array.
To find the parent of this new node, we use the formula (len(array) - 1) // 2
. Why does this work? Because if the new node will be at index n
, its parent is at index (n-1) // 2
. Since we haven't added the node yet, n = len(array)
, so the parent is at (len(array) - 1) // 2
.
The initialization uses BFS to convert the existing tree into this array representation. Once we have this array, insertions become trivial - just append to the array and connect to the parent node. We can determine whether the new node should be a left or right child by checking if the parent already has a left child.
This approach brilliantly transforms a tree manipulation problem into simple array operations with O(1) insertion complexity.
Learn more about Tree, Breadth-First Search and Binary Tree patterns.
Solution Approach
The solution uses an array-based representation of the complete binary tree, leveraging BFS for initialization and mathematical indexing for efficient insertions.
Initialization (__init__
)
We use BFS to traverse the existing complete binary tree and store all nodes in an array self.tree
:
- Create an empty array
self.[tree](/problems/tree_intro)
to store all nodes - Initialize a queue with the root node
- Process nodes level by level using BFS:
- For each level, process all nodes in the queue
- Add each node to
self.tree
array - Add the node's children (if they exist) to the queue for the next level
q = deque([root])
while q:
for _ in range(len(q)):
node = q.popleft()
self.[tree](/problems/tree_intro).append(node)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
After initialization, self.[tree](/problems/tree_intro)
contains all nodes in level-order sequence.
Insertion (insert
)
To insert a new node while maintaining completeness:
-
Calculate the parent node's index:
p = self.[tree](/problems/tree_intro)[(len(self.tree) - 1) // 2]
- If the new node will be at index
n
, its parent is at(n-1) // 2
- Since
n = len(self.tree)
(before insertion), parent index is(len(self.tree) - 1) // 2
- If the new node will be at index
-
Create the new node:
node = TreeNode(val)
-
Append the new node to the array:
self.[tree](/problems/tree_intro).append(node)
-
Connect the new node to its parent:
- If parent has no left child, make the new node the left child
- Otherwise, make it the right child
-
Return the parent's value
Get Root (get_root
)
Simply return the first element of the array: return self.[tree](/problems/tree_intro)[0]
Time and Space Complexity
- Initialization: O(n) time and O(n) space, where n is the number of nodes in the initial tree
- Insertion: O(1) time for each insertion
- Get Root: O(1) time
The beauty of this approach is that it transforms tree operations into simple array operations by maintaining the heap property, where parent-child relationships are determined by array indices rather than explicit pointers.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate how the solution works.
Initial tree:
1 / \ 2 3 / 4
Step 1: Initialization with CBTInserter(root)
We perform BFS traversal to build our array representation:
- Start with queue = [1]
- Process level 1: Add node 1 to array, queue becomes [2, 3]
- Process level 2: Add nodes 2 and 3 to array, queue becomes [4]
- Process level 3: Add node 4 to array, queue becomes empty
Result: self.tree = [1, 2, 3, 4]
(nodes stored by reference, showing values for clarity)
The array indices correspond to:
- Index 0: node 1 (root)
- Index 1: node 2 (left child of 1)
- Index 2: node 3 (right child of 1)
- Index 3: node 4 (left child of 2)
Step 2: Insert value 5
-
Calculate parent index:
(len(self.tree) - 1) // 2 = (4 - 1) // 2 = 1
- Parent is at index 1, which is node 2
-
Create new node with value 5
-
Append to array:
self.tree = [1, 2, 3, 4, 5]
-
Connect to parent:
- Node 2 already has a left child (node 4)
- So make node 5 the right child of node 2
-
Return parent value: 2
Tree after insertion:
1 / \ 2 3 / \ 4 5
Step 3: Insert value 6
-
Calculate parent index:
(5 - 1) // 2 = 2
- Parent is at index 2, which is node 3
-
Create new node with value 6
-
Append to array:
self.tree = [1, 2, 3, 4, 5, 6]
-
Connect to parent:
- Node 3 has no left child
- So make node 6 the left child of node 3
-
Return parent value: 3
Final tree:
1 / \ 2 3 / \ / 4 5 6
The array [1, 2, 3, 4, 5, 6]
perfectly represents the complete binary tree, where:
- Node at index i has parent at index (i-1)//2
- Node at index i has left child at index 2i+1 and right child at index 2i+2
This demonstrates how the array-based approach elegantly maintains the complete binary tree property with O(1) insertions.
Solution Implementation
1from collections import deque
2from typing import Optional
3
4# Definition for a binary tree node.
5class TreeNode:
6 def __init__(self, val=0, left=None, right=None):
7 self.val = val
8 self.left = left
9 self.right = right
10
11class CBTInserter:
12 """
13 A class to handle insertions in a Complete Binary Tree (CBT).
14 Maintains the tree nodes in level-order traversal for efficient insertion.
15 """
16
17 def __init__(self, root: Optional[TreeNode]):
18 """
19 Initialize the CBT inserter with an existing complete binary tree.
20 Stores all nodes in a list following level-order traversal.
21
22 Args:
23 root: The root node of the existing complete binary tree
24 """
25 # Store all tree nodes in level-order (breadth-first) sequence
26 self.tree_nodes = []
27
28 # Use BFS to traverse the tree and store nodes in order
29 queue = deque([root])
30 while queue:
31 # Process all nodes at current level
32 level_size = len(queue)
33 for _ in range(level_size):
34 current_node = queue.popleft()
35 self.tree_nodes.append(current_node)
36
37 # Add children to queue for next level processing
38 if current_node.left:
39 queue.append(current_node.left)
40 if current_node.right:
41 queue.append(current_node.right)
42
43 def insert(self, val: int) -> int:
44 """
45 Insert a new node with given value into the complete binary tree.
46
47 Args:
48 val: The value for the new node to be inserted
49
50 Returns:
51 The value of the parent node where the new node was inserted
52 """
53 # Calculate parent index using complete binary tree property
54 # For 0-indexed array: parent of node at index i is at (i-1)//2
55 parent_index = (len(self.tree_nodes) - 1) // 2
56 parent_node = self.tree_nodes[parent_index]
57
58 # Create the new node
59 new_node = TreeNode(val)
60 self.tree_nodes.append(new_node)
61
62 # Attach new node to parent's left or right based on availability
63 if parent_node.left is None:
64 parent_node.left = new_node
65 else:
66 parent_node.right = new_node
67
68 return parent_node.val
69
70 def get_root(self) -> Optional[TreeNode]:
71 """
72 Get the root node of the complete binary tree.
73
74 Returns:
75 The root node of the tree
76 """
77 return self.tree_nodes[0] if self.tree_nodes else None
78
79
80# Your CBTInserter object will be instantiated and called as such:
81# obj = CBTInserter(root)
82# param_1 = obj.insert(val)
83# param_2 = obj.get_root()
84
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 */
16
17/**
18 * Complete Binary Tree Inserter
19 * Maintains a complete binary tree and supports insertion operations
20 */
21class CBTInserter {
22 // List to store all tree nodes in level-order traversal sequence
23 private List<TreeNode> treeNodes;
24
25 /**
26 * Constructor initializes the CBT inserter with an existing complete binary tree
27 * @param root The root of the initial complete binary tree
28 */
29 public CBTInserter(TreeNode root) {
30 treeNodes = new ArrayList<>();
31
32 // Perform level-order traversal to store all nodes
33 Deque<TreeNode> queue = new ArrayDeque<>();
34 queue.offer(root);
35
36 while (!queue.isEmpty()) {
37 int levelSize = queue.size();
38
39 // Process all nodes at current level
40 for (int i = 0; i < levelSize; i++) {
41 TreeNode currentNode = queue.poll();
42 treeNodes.add(currentNode);
43
44 // Add children to queue for next level processing
45 if (currentNode.left != null) {
46 queue.offer(currentNode.left);
47 }
48 if (currentNode.right != null) {
49 queue.offer(currentNode.right);
50 }
51 }
52 }
53 }
54
55 /**
56 * Inserts a new node with given value into the complete binary tree
57 * @param val The value to be inserted
58 * @return The value of the parent node of the newly inserted node
59 */
60 public int insert(int val) {
61 // Find the parent node for the new insertion
62 // In a complete binary tree stored as array: parent index = (childIndex - 1) / 2
63 int parentIndex = (treeNodes.size() - 1) / 2;
64 TreeNode parentNode = treeNodes.get(parentIndex);
65
66 // Create new node and add to the list
67 TreeNode newNode = new TreeNode(val);
68 treeNodes.add(newNode);
69
70 // Attach new node to its parent
71 // If parent's left child is null, attach as left child; otherwise as right child
72 if (parentNode.left == null) {
73 parentNode.left = newNode;
74 } else {
75 parentNode.right = newNode;
76 }
77
78 return parentNode.val;
79 }
80
81 /**
82 * Returns the root of the complete binary tree
83 * @return The root node of the tree
84 */
85 public TreeNode get_root() {
86 return treeNodes.get(0);
87 }
88}
89
90/**
91 * Your CBTInserter object will be instantiated and called as such:
92 * CBTInserter obj = new CBTInserter(root);
93 * int param_1 = obj.insert(val);
94 * TreeNode param_2 = obj.get_root();
95 */
96
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 CBTInserter {
13public:
14 /**
15 * Constructor: Initialize the complete binary tree inserter with an existing tree
16 * Uses BFS to traverse the tree and store all nodes in a vector for O(1) parent access
17 * @param root The root of the existing complete binary tree
18 */
19 CBTInserter(TreeNode* root) {
20 // Use BFS to traverse the tree level by level
21 std::queue<TreeNode*> bfsQueue;
22 bfsQueue.push(root);
23
24 // Process all nodes level by level
25 while (!bfsQueue.empty()) {
26 int levelSize = bfsQueue.size();
27
28 // Process all nodes at current level
29 for (int i = 0; i < levelSize; ++i) {
30 TreeNode* currentNode = bfsQueue.front();
31 bfsQueue.pop();
32
33 // Store node in vector for array-based representation
34 treeNodes.push_back(currentNode);
35
36 // Add children to queue for next level processing
37 if (currentNode->left) {
38 bfsQueue.push(currentNode->left);
39 }
40 if (currentNode->right) {
41 bfsQueue.push(currentNode->right);
42 }
43 }
44 }
45 }
46
47 /**
48 * Insert a new node with given value into the complete binary tree
49 * Maintains complete binary tree property by inserting at the next available position
50 * @param val The value of the new node to insert
51 * @return The value of the parent node of the newly inserted node
52 */
53 int insert(int val) {
54 // Calculate parent index using complete binary tree property
55 // For 0-indexed array: parent of node at index i is at index (i-1)/2
56 int parentIndex = (treeNodes.size() - 1) / 2;
57 TreeNode* parentNode = treeNodes[parentIndex];
58
59 // Create new node with given value
60 TreeNode* newNode = new TreeNode(val);
61
62 // Add new node to the vector to maintain array representation
63 treeNodes.push_back(newNode);
64
65 // Attach new node to parent's left or right based on availability
66 if (!parentNode->left) {
67 parentNode->left = newNode;
68 } else {
69 parentNode->right = newNode;
70 }
71
72 return parentNode->val;
73 }
74
75 /**
76 * Get the root node of the complete binary tree
77 * @return Pointer to the root node
78 */
79 TreeNode* get_root() {
80 return treeNodes[0];
81 }
82
83private:
84 // Vector storing all tree nodes in level-order for O(1) parent access
85 // Enables array-based representation of complete binary tree
86 std::vector<TreeNode*> treeNodes;
87};
88
89/**
90 * Your CBTInserter object will be instantiated and called as such:
91 * CBTInserter* obj = new CBTInserter(root);
92 * int param_1 = obj->insert(val);
93 * TreeNode* param_2 = obj->get_root();
94 */
95
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 * val: number
5 * left: TreeNode | null
6 * right: TreeNode | null
7 * constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 * this.val = (val===undefined ? 0 : val)
9 * this.left = (left===undefined ? null : left)
10 * this.right = (right===undefined ? null : right)
11 * }
12 * }
13 */
14
15// Array to store all nodes in level-order traversal
16let treeNodes: TreeNode[] = [];
17
18/**
19 * Initializes the complete binary tree inserter with an existing tree
20 * Performs level-order traversal to populate the nodes array
21 * @param root - The root node of the existing complete binary tree
22 */
23function CBTInserterConstructor(root: TreeNode | null): void {
24 // Reset the tree nodes array
25 treeNodes = [];
26
27 // Handle empty tree case
28 if (root === null) {
29 return;
30 }
31
32 // Initialize queue for level-order traversal
33 const queue: TreeNode[] = [root];
34
35 // Perform level-order traversal
36 while (queue.length > 0) {
37 const nextLevel: TreeNode[] = [];
38
39 // Process all nodes at current level
40 for (const currentNode of queue) {
41 // Add current node to the array
42 treeNodes.push(currentNode);
43
44 // Add children to next level if they exist
45 if (currentNode.left !== null) {
46 nextLevel.push(currentNode.left);
47 }
48 if (currentNode.right !== null) {
49 nextLevel.push(currentNode.right);
50 }
51 }
52
53 // Replace queue contents with next level nodes
54 queue.splice(0, queue.length, ...nextLevel);
55 }
56}
57
58/**
59 * Inserts a new node into the complete binary tree
60 * The new node is inserted at the next available position to maintain completeness
61 * @param val - The value of the new node to insert
62 * @returns The value of the parent node where the new node was inserted
63 */
64function insert(val: number): number {
65 // Calculate parent index using complete binary tree property
66 // Parent of node at index i is at index (i-1)/2
67 const parentIndex = (treeNodes.length - 1) >> 1;
68 const parentNode = treeNodes[parentIndex];
69
70 // Create the new node
71 const newNode = new TreeNode(val);
72
73 // Add new node to the array
74 treeNodes.push(newNode);
75
76 // Attach new node to parent
77 // If parent's left child is null, attach to left; otherwise attach to right
78 if (parentNode.left === null) {
79 parentNode.left = newNode;
80 } else {
81 parentNode.right = newNode;
82 }
83
84 // Return parent's value
85 return parentNode.val;
86}
87
88/**
89 * Returns the root node of the complete binary tree
90 * @returns The root node of the tree, or null if tree is empty
91 */
92function get_root(): TreeNode | null {
93 return treeNodes.length > 0 ? treeNodes[0] : null;
94}
95
Time and Space Complexity
Time Complexity:
-
Initialization (
__init__
):O(n)
wheren
is the number of nodes in the initial tree. The constructor performs a level-order traversal using BFS to visit all nodes exactly once and append them to theself.tree
list. -
Insert (
insert
):O(1)
. The method calculates the parent index using the formula(len(self.tree) - 1) // 2
, creates a new node, appends it to the list, and updates the parent's child pointer. All these operations take constant time. -
Get Root (
get_root
):O(1)
. Simply returns the first element of theself.tree
list, which is a constant time operation.
Space Complexity:
O(n)
wheren
is the total number of nodes in the tree. Theself.tree
list stores references to all nodes in the complete binary tree. Additionally, during initialization, the BFS queue temporarily holds at mostO(n)
nodes in the worst case (though typically much less for a complete binary tree where the last level contains about half the nodes).
The key insight is that by maintaining all nodes in an array representation of the complete binary tree, we can leverage the mathematical relationship between parent and child indices (parent at index i
has children at indices 2i+1
and 2i+2
) to achieve constant-time insertions while preserving the complete binary tree property.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Parent Index Calculation
A common mistake is using the wrong formula for finding the parent index, especially when dealing with 0-indexed vs 1-indexed arrays.
Pitfall Example:
# Wrong: Using 1-indexed formula in 0-indexed array
parent_index = len(self.tree_nodes) // 2 # Missing the -1
# Wrong: Off-by-one error
parent_index = (len(self.tree_nodes)) // 2 # Should be (len - 1) // 2
Correct Approach:
# For 0-indexed array where new node will be at index len(tree_nodes)
parent_index = (len(self.tree_nodes) - 1) // 2
2. Not Handling Empty Tree Initialization
The code assumes the root is never None
, but this could cause issues if an empty tree is passed.
Pitfall Example:
def __init__(self, root: Optional[TreeNode]):
self.tree_nodes = []
queue = deque([root]) # If root is None, this adds None to queue
while queue:
# Will process None and cause errors
Solution:
def __init__(self, root: Optional[TreeNode]):
self.tree_nodes = []
if root is None:
return
queue = deque([root])
# ... rest of the BFS code
3. Forgetting to Update Parent's Child Pointers
A critical mistake is adding the new node to the array but forgetting to update the parent node's left/right pointers.
Pitfall Example:
def insert(self, val: int) -> int:
parent_index = (len(self.tree_nodes) - 1) // 2
parent_node = self.tree_nodes[parent_index]
new_node = TreeNode(val)
self.tree_nodes.append(new_node)
# Forgot to set parent_node.left or parent_node.right!
return parent_node.val
4. Using Wrong Data Structure for Level-Order Traversal
Using a list instead of deque for BFS can lead to inefficient O(n) operations when removing from the front.
Pitfall Example:
# Inefficient: Using list.pop(0) is O(n) queue = [root] while queue: node = queue.pop(0) # O(n) operation
Better Approach:
# Efficient: Using deque.popleft() is O(1) queue = deque([root]) while queue: node = queue.popleft() # O(1) operation
5. Misunderstanding the Parent-Child Relationship
Confusion about when to attach as left vs right child, especially with the array indexing approach.
Key Understanding:
- In a 0-indexed array representation:
- Node at index
i
has left child at2*i + 1
and right child at2*i + 2
- When inserting at position
n
, ifn
is odd, it's a left child; if even, it's a right child
- Node at index
Reliable Check:
if parent_node.left is None: parent_node.left = new_node else: parent_node.right = new_node
This approach is more reliable than trying to determine left/right based on the index arithmetic.
Which type of traversal does breadth first search do?
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 assets algo monster 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 Breadth First Search BFS
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!