Files
codetutor/backend/data/questions/binary-search-tree-iterator.yaml

212 lines
9.9 KiB
YAML

title: Binary Search Tree Iterator
slug: binary-search-tree-iterator
difficulty: medium
leetcode_id: 173
leetcode_url: https://leetcode.com/problems/binary-search-tree-iterator/
categories:
- trees
- stack
patterns:
- tree-traversal
- monotonic-stack
description: |
Implement the `BSTIterator` class that represents an iterator over the **in-order traversal** of a binary search tree (BST):
- `BSTIterator(TreeNode root)` Initializes an object of the `BSTIterator` class. The `root` of the BST is given as part of the constructor. The pointer should be initialized to a non-existent number smaller than any element in the BST.
- `boolean hasNext()` Returns `true` if there exists a number in the traversal to the right of the pointer, otherwise returns `false`.
- `int next()` Moves the pointer to the right, then returns the number at the pointer.
Notice that by initializing the pointer to a non-existent smallest number, the first call to `next()` will return the smallest element in the BST.
You may assume that `next()` calls will always be valid. That is, there will be at least a next number in the in-order traversal when `next()` is called.
constraints: |
- `1 <= number of nodes <= 10^5`
- `0 <= Node.val <= 10^6`
- At most `10^5` calls will be made to `hasNext` and `next`
examples:
- input: |
["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
[[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]
output: "[null, 3, 7, true, 9, true, 15, true, 20, false]"
explanation: |
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next(); // return 3
bSTIterator.next(); // return 7
bSTIterator.hasNext(); // return True
bSTIterator.next(); // return 9
bSTIterator.hasNext(); // return True
bSTIterator.next(); // return 15
bSTIterator.hasNext(); // return True
bSTIterator.next(); // return 20
bSTIterator.hasNext(); // return False
explanation:
intuition: |
Imagine you're reading a book where each page references other pages before and after it. An in-order traversal of a BST visits nodes in **sorted order** (left subtree → node → right subtree). The challenge is to deliver this sorted sequence one element at a time, on demand, without storing the entire traversal upfront.
Think of it like this: instead of printing all nodes at once during a traversal, you want to **pause** the traversal after each node and resume when the caller asks for the next value. This is the essence of an iterator pattern.
The key insight is that a recursive in-order traversal uses the **call stack** to remember where to return. We can simulate this explicitly using our own stack. At any point, the stack holds the path of "left ancestors" — nodes we've visited but haven't processed yet because we went left first.
When we need the next element, we pop from the stack (that's our current node), then push all nodes along the left path of the right subtree. This controlled traversal gives us O(h) memory usage and amortized O(1) time per `next()` call.
approach: |
We solve this using a **Controlled In-Order Traversal with Stack**:
**Step 1: Initialize the iterator**
- Create an empty stack to simulate the call stack of recursive traversal
- Call a helper function `_push_left(node)` starting from the root
- This helper pushes the node and all its left descendants onto the stack
- After initialization, the stack contains the leftmost path from root
&nbsp;
**Step 2: Implement `_push_left` helper**
- While the current node is not null:
- Push the node onto the stack
- Move to the left child
- This ensures we always have the "next smallest" nodes ready
&nbsp;
**Step 3: Implement `next()`**
- Pop the top node from the stack — this is the next smallest element
- If this node has a right child, call `_push_left(right_child)`
- This prepares the stack for future calls by adding the right subtree's leftmost path
- Return the popped node's value
&nbsp;
**Step 4: Implement `hasNext()`**
- Simply check if the stack is non-empty
- If there are nodes on the stack, there are more elements to iterate
&nbsp;
This approach achieves O(h) space complexity where h is the tree height, and amortized O(1) time per operation because each node is pushed and popped exactly once across all operations.
common_pitfalls:
- title: Flattening the Entire Tree Upfront
description: |
A naive approach is to perform a complete in-order traversal in the constructor and store all values in a list:
```python
def __init__(self, root):
self.values = []
self._inorder(root) # O(n) time and space
self.index = 0
```
While this works and gives O(1) for `next()` and `hasNext()`, it uses **O(n) space** to store all node values. The follow-up challenge asks for O(h) space, which is more memory-efficient for tall, sparse trees.
wrong_approach: "Store all values in a list during construction"
correct_approach: "Use a stack to control traversal on-demand"
- title: Forgetting to Process Right Subtrees
description: |
After popping a node in `next()`, you must check if it has a right child. If you forget this step, you'll skip all nodes in right subtrees.
For example, in a tree `[2, 1, 3]`, after returning `1` (left child), if you don't push `3` (right child of root), you'll miss it entirely.
wrong_approach: "Only pop from stack without handling right children"
correct_approach: "After popping, push the left path of the right subtree"
- title: Incorrect Stack Initialization
description: |
The stack must be initialized with the leftmost path from the root, not just the root itself. If you only push the root, the first `next()` call won't return the smallest element.
For tree `[7, 3, 15]`, the first `next()` should return `3`, not `7`. This requires pushing `7` then `3` during initialization.
wrong_approach: "Push only the root node"
correct_approach: "Push all nodes along the leftmost path"
key_takeaways:
- "**Iterator pattern**: Controlled traversal delivers elements on-demand without pre-computing the entire sequence"
- "**Explicit stack**: Simulates the call stack of recursive traversal, giving you control over when to pause and resume"
- "**Amortized analysis**: Each node is pushed and popped exactly once, so n operations cost O(n) total — O(1) amortized per operation"
- "**Space optimization**: O(h) stack space is better than O(n) for balanced trees (h = log n) and acceptable for skewed trees (h = n)"
time_complexity: "O(1) amortized per operation. Each node is pushed and popped from the stack exactly once across all `next()` calls, so n calls take O(n) total time."
space_complexity: "O(h) where h is the height of the tree. The stack holds at most h nodes (the leftmost path). For a balanced tree, h = O(log n); for a skewed tree, h = O(n)."
solutions:
- approach_name: Controlled Traversal with Stack
is_optimal: true
code: |
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: TreeNode):
# Stack to simulate the call stack of recursive traversal
self.stack = []
# Push all left nodes from root to leftmost leaf
self._push_left(root)
def _push_left(self, node: TreeNode) -> None:
"""Push node and all its left descendants onto the stack."""
while node:
self.stack.append(node)
node = node.left
def next(self) -> int:
# Pop the next smallest element
node = self.stack.pop()
# If it has a right subtree, push its leftmost path
if node.right:
self._push_left(node.right)
return node.val
def hasNext(self) -> bool:
# There are more elements if stack is non-empty
return len(self.stack) > 0
explanation: |
**Time Complexity:** O(1) amortized per operation — each node pushed/popped once.
**Space Complexity:** O(h) — stack holds at most the tree height.
The stack maintains the "frontier" of unvisited nodes along the leftmost path. Each `next()` call advances the traversal by one step, lazily exploring right subtrees only when needed.
- approach_name: Flatten to List
is_optimal: false
code: |
class BSTIterator:
def __init__(self, root: TreeNode):
# Store all values from in-order traversal
self.values = []
self.index = 0
self._inorder(root)
def _inorder(self, node: TreeNode) -> None:
"""Recursively collect all values in sorted order."""
if not node:
return
self._inorder(node.left)
self.values.append(node.val)
self._inorder(node.right)
def next(self) -> int:
# Return current value and advance pointer
val = self.values[self.index]
self.index += 1
return val
def hasNext(self) -> bool:
# Check if we've exhausted the list
return self.index < len(self.values)
explanation: |
**Time Complexity:** O(n) for constructor, O(1) for `next()` and `hasNext()`.
**Space Complexity:** O(n) — stores all node values.
This approach pre-computes the entire traversal. It's simpler to implement but uses more memory. Useful when you know you'll iterate through all elements anyway, but doesn't meet the O(h) space constraint in the follow-up.