BST Iterator Design in DSA Python - Time & Space Complexity
We want to understand how fast the BST Iterator works when we ask for the next smallest element.
How does the time to get each next element change as the tree grows?
Analyze the time complexity of the following BST Iterator code snippet.
class BSTIterator:
def __init__(self, root):
self.stack = []
self._push_left(root)
def _push_left(self, node):
while node:
self.stack.append(node)
node = node.left
def next(self):
node = self.stack.pop()
self._push_left(node.right)
return node.val
def hasNext(self):
return len(self.stack) > 0
This code creates an iterator that returns the next smallest value in a binary search tree each time next() is called.
Look at what repeats when we call next() multiple times.
- Primary operation: Pushing left children of a node onto the stack.
- How many times: Each node is pushed and popped exactly once during the entire iteration.
Each node is handled once, but the work per next() call varies.
| Input Size (n) | Approx. Operations per next() |
|---|---|
| 10 | About 1 push/pop on average |
| 100 | About 1 push/pop on average |
| 1000 | About 1 push/pop on average |
Pattern observation: Although some next() calls do more work, on average each call does a small constant amount of work.
Time Complexity: O(1) amortized per next() call
This means each call to next() takes constant time on average, even if some calls do more work.
[X] Wrong: "Each next() call always takes O(h) time because of the while loop."
[OK] Correct: The while loop work is spread out over all nodes, so each node is pushed once. This means the average time per next() is constant, not O(h).
Understanding amortized time helps you explain why some operations seem slow sometimes but are fast overall. This skill shows you can analyze real-world data structures deeply.
What if we changed the iterator to store all nodes in a list upfront? How would the time complexity of next() change?
Practice
BST Iterator in a binary search tree?Solution
Step 1: Understand BST Iterator functionality
A BST Iterator is designed to traverse the tree in sorted order, typically ascending, without using recursion.Step 2: Identify the main purpose
It provides a way to get the next smallest element efficiently, which matches To traverse the BST in ascending order without recursion.Final Answer:
To traverse the BST in ascending order without recursion -> Option CQuick Check:
BST Iterator = In-order traversal without recursion [OK]
- Confusing iterator with insertion or deletion
- Thinking it balances the tree automatically
- Assuming it uses recursion internally
Solution
Step 1: Identify the correct data structure for stack
In Python, a stack is commonly implemented using a list, which supports push and pop operations.Step 2: Match the syntax
An empty list is created with[], sostack = []is correct.Final Answer:
stack = [] -> Option AQuick Check:
Stack uses list [] in Python [OK]
- Using {} which creates a dictionary, not a stack
- Using () which creates a tuple, immutable
- Using set() which is unordered
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class BSTIterator:
def __init__(self, root):
self.stack = []
self._push_left(root)
def _push_left(self, node):
while node:
self.stack.append(node)
node = node.left
def next(self):
node = self.stack.pop()
self._push_left(node.right)
return node.val
root = TreeNode(2, TreeNode(1), TreeNode(3))
iterator = BSTIterator(root)
print(iterator.next())
print(iterator.next())
print(iterator.next())Solution
Step 1: Understand the BST structure and iterator initialization
The BST has root 2, left child 1, right child 3. The iterator pushes all left nodes starting from root, so stack = [2,1].Step 2: Trace the next() calls
First next(): pop 1, push left of 1's right (None), output 1.
Second next(): pop 2, push left of 2's right (3), stack = [3], output 2.
Third next(): pop 3, push left of 3's right (None), output 3.Final Answer:
1 2 3 -> Option BQuick Check:
In-order traversal = 1, 2, 3 [OK]
- Confusing order of popping from stack
- Not pushing left children of right subtree
- Assuming preorder or postorder traversal
class BSTIterator:
def __init__(self, root):
self.stack = []
self._push_left(root)
def _push_left(self, node):
while node:
self.stack.append(node)
node = node.right # Bug here
def next(self):
node = self.stack.pop()
self._push_left(node.right)
return node.valSolution
Step 1: Analyze the _push_left method
The method is supposed to push all left children to the stack to simulate in-order traversal, but it incorrectly moves to the right child in the loop.Step 2: Identify the correct traversal direction
It should move to the left child (node = node.left) to push left nodes, so In _push_left, should traverse left child, not right child is correct.Final Answer:
In _push_left, should traverse left child, not right child -> Option AQuick Check:
_push_left must go left, not right [OK]
- Confusing left and right child traversal
- Using set instead of list for stack
- Returning wrong attribute from node
hasNext() and next() methods. Which approach best balances time and space complexity for large BSTs?Solution
Step 1: Evaluate storing all nodes upfront
Storing all nodes in a list uses O(n) space and time upfront, which is inefficient for large trees.Step 2: Consider stack-based on-demand traversal
Using a stack to push left nodes only when needed uses O(h) space (h = tree height) and provides efficient next() calls.Step 3: Analyze recursion and root-only traversal
Recursion each next() call is inefficient and root-only traversal repeats work, both are poor choices.Final Answer:
Use a stack to push left nodes on demand during iteration -> Option DQuick Check:
Stack-based lazy traversal balances time and space [OK]
- Preloading entire tree wastes memory
- Using recursion repeatedly causes overhead
- Restarting traversal from root wastes time
Ready to prove it?
Pass the test and this pattern moves unknown
