BST Iterator
BST Iterator
Implement an iterator over a Binary Search Tree (BST) that returns nodes in ascending order (inorder traversal).
Why this is worth learning
An iterator stores a suspended traversal, not a precomputed sorted array. Its stack is the frontier needed to produce exactly the next inorder value.
Before coding: one-minute experiment
Can one Next call take O(h) work even though Next is described as amortized O(1)?
Function signature
type BSTIterator struct { /* fields */ }
func Constructor(root *TreeNode) BSTIterator
func (it *BSTIterator) Next() int
func (it *BSTIterator) HasNext() bool
Inputs / outputs
Constructor(root): initialize an iterator from the BST root.Next(): return the next smallest value.HasNext(): return true if a next value exists.
Example
7
/ \
3 15
/ \
9 20
Calls: Next(), Next(), HasNext(), Next(), HasNext()
Returns: 3, 7, true, 9, true
Constraints
- 0 <= number of nodes <= 100_000
Next()is only called ifHasNext()is true
Core idea (near-solution)
A sorted slice would give simple Next calls but would traverse and store the entire tree before the first result. A lazy iterator instead preserves just the work recursion has postponed: the ancestors on the route to the next inorder node. Constructor pushes the left spine. Popping its top emits the next value; the popped node's right subtree contributes its own left spine. This produces the same sorted stream while retaining O(h) state. HasNext observes that state and must not change it; otherwise checking availability would consume an element.
Invariant: the stack top is always the next smallest node.
Algorithm (step-by-step)
Constructor: push all left children fromrootto the stack.HasNext: returnstacknot empty.Next:- Pop the top node.
- Push the left spine of its right child.
- Return the popped value.
Correctness sketch
After construction, pushing the root's left spine puts the smallest node at the stack top. Before each Next, that top is the smallest unvisited node. Pop it and push the left spine of its right subtree; those nodes are the next candidates after the popped value, while older ancestors remain below them. This restores the frontier invariant. HasNext is true exactly when a candidate remains on the stack. Each tree node is pushed once and popped once over a full traversal, giving linear total work despite occasional Next calls pushing several nodes. Independent iterator instances retain independent frontiers.
Trace (hand walkthrough)
Example input:
7
/ \
3 15
/ \
9 20
Calls: Next(), Next(), HasNext(), Next(), HasNext()
Returns: 3, 7, true, 9, true
Walk:
- init stack with left spine: [7,3] (top=3)
- Next()->3; stack=[7]
- Next()->7; push left spine of 15 -> stack=[15,9]
- HasNext()->true; Next()->9; HasNext()->true
Pitfalls to avoid
- Forgetting to push the left spine of
node.RightinNext(). - Pushing the entire subtree (should only push the left spine).
- Returning values without checking
HasNext()(assume caller follows contract).
Debugging lens
Interleave two iterators over the same tree, repeat HasNext without consuming values, and exhaust an empty iterator without calling Next. The tree must remain unchanged.
Complexity
Constructor:O(h)time,O(h)space.- Each
Next: amortizedO(1)time,O(h)space total.
Implementation notes (Go)
- Use a
[]*TreeNodeslice as a stack and a helperpushLeft(node). - Handle
root == nilby leaving the stack empty.
Reconstruction checkpoint
An iterator has pending stack [10,3] bottom-to-top, and node 3 has right subtree rooted at 8 with left child 5. After Next, what value and stack result? What can two consecutive HasNext calls change?
Model answer
Next returns 3, pops it, and pushes 8 then 5, leaving [10,8,5]. The next returned value will be 5. HasNext can only report that the stack is nonempty; neither call may pop or push. This Next did several operations, which is compatible with amortized constant cost over a traversal.