Iterator
Topic Type: Data Structure — a stateful object that exposes a sequential access API over an underlying collection or computation.
LeetCode tag size: ~35–50 problems. Depth: core concepts, main patterns, key complexities.
1. Core Concepts
Iterator — an object that produces elements one at a time on demand, maintaining a cursor into an underlying sequence. Separates traversal logic from collection structure.
Iterator protocol — the minimal API: next() returns the next element and advances the cursor; hasNext() returns whether any elements remain. Some variants add peek() without advancing.
Lazy evaluation — elements are computed or fetched only when next() is called, not upfront. Enables iterating over infinite or very large sequences with O(1) space per step.
External iterator — the caller controls traversal by explicitly calling next(). Enables interleaving multiple iterators (zip, merge) and peeking without consuming.
Internal iterator — traversal is controlled by the collection (e.g., Python for x in collection). Not directly composable with other iterators.
Peeking — reading the next element without consuming it. Requires buffering exactly one element. Used in merge logic to compare heads of multiple iterators.
Flattening — converting a nested or two-dimensional structure into a linear sequence. Depth-first by default; requires a stack or recursion to defer inner iterators.
Buffering / prefetch — storing one or more pre-fetched elements to support peek() or hasNext() when the underlying source is consumed-on-read (e.g., a generator or file stream).
Exhaustion — the state when hasNext() returns False. Once exhausted, calling next() is undefined behavior in most designs; guard with hasNext() first.
Composite / wrapping iterator — an iterator built on top of one or more existing iterators. The outer iterator holds references to inner iterators and delegates element production to them.
2. Structural Invariants
| Property | Rule | What breaks if violated |
|---|---|---|
| Cursor monotonicity | The internal cursor only moves forward; next() never rewinds | Elements are skipped or repeated |
| Peek idempotence | Calling peek() any number of times without next() returns the same element | Caller loses elements or gets wrong merge decisions |
hasNext() purity | hasNext() must not advance the cursor or consume elements | Calling hasNext() twice skips an element |
| Buffer consistency | If a peeked value is buffered, next() must return the buffer and clear it before fetching a new element | Double-consumption; the buffered element is lost |
| Exhaustion finality | Once hasNext() returns False, it must not return True again | Caller loops forever or misses a sentinel |
| Nested iterator stack order | In flatten, the top of the stack holds the iterator currently being consumed | Wrong DFS order; inner lists partially skipped |
3. Internal Representation
Single iterator with peek buffer:
self._buffer = None # None means no buffered element
self._exhausted = False
hasNext() tries to fill the buffer from the underlying source if empty; next() drains the buffer first.
Flatten with explicit stack: Store a stack of iterators. The top entry is the currently-active iterator. When it exhausts, pop and resume the parent. This mirrors the call stack of recursion but makes state explicit and restartable.
Flatten via generator (yield from):
Python generators maintain their own frame as the cursor. yield from inner flattens one level; nesting yield from calls recursively flattens arbitrarily deep structures. The generator frame is the buffer.
k-way merge with min-heap:
Store (value, iterator_index) tuples in a heap. The heap maintains the globally-smallest unread element at the top. When popped, the same iterator advances and re-inserts its next element.
Compressed / encoded iterator (RLE, combination):
Internal state includes the current segment (value + remaining count) or a combinatorial index. next() decrements a counter or increments an index before yielding.
4. Types / Variants
| Variant | What it does | When to use |
|---|---|---|
| Peek wrapper | Wraps any iterator, adds peek() via a one-element buffer | Merge logic that must compare heads without consuming |
| Flatten iterator | Linearises a nested structure (list of lists, nested list) | When input depth is unknown or variable (e.g., NestedInteger) |
| Merge / k-way merge | Interleaves k sorted iterators in sorted order | Merging sorted streams, external sort, priority-based scheduling |
| Zigzag / round-robin | Yields alternately from two or more iterators | Interleaving iterators without ordering constraint (LeetCode 281) |
| RLE / compressed iterator | Decodes a run-length or encoded stream element-by-element | Space-efficient iteration over repeated-value sequences |
| Combination / permutation iterator | Generates the next combination or permutation lexicographically | Iterating over search spaces without materialising all at once |
| BST iterator | In-order traversal of a BST one node at a time | O(h) space in-order traversal without full recursion upfront |
5. Topic-Specific Operations
__init__(source) — initialise internal state (stack, buffer, heap). May advance the underlying source to position the cursor at the first valid element. For flatten, push the outermost iterator; for merge, heapify initial elements.
hasNext() → bool — determine whether any unconsumed element exists. For peek-buffered designs, attempt to fill the buffer here; return whether the buffer is non-empty. Must not mutate visible state. O(1) amortised (may do work lazily).
next() → element — return and consume the current element; advance cursor to the next. If a peek buffer exists, drain it first. For flatten, advance the top-of-stack iterator; if it exhausts, pop and continue. O(1) amortised for most designs; O(log k) for k-way merge.
peek() → element — return the next element without consuming it. If buffer is empty, call next() on the underlying source and store the result. Subsequent calls return the buffer. Edge case: calling peek() on an exhausted iterator is undefined — always guard with hasNext().
6. Key Algorithms
Peek Buffer (one-element lookahead)
Wraps an existing iterator to add peek(). Maintains a _peeked flag and _val cache; next() checks the flag first.
# inside next():
if self._peeked:
self._peeked = False; return self._val
return iter(self._source).__next__()
Time: O(1) per operation. Space: O(1) buffer.
Flatten with Stack (iterative DFS)
Solves LeetCode 341 (Flatten Nested List Iterator). Push the top-level iterator onto a stack. hasNext() advances the stack until the front element is an integer.
# inside hasNext():
while stack and not stack[-1].isInteger():
stack.extend(stack.pop().getList())
Time: O(1) amortised per next() call (each element processed once). Space: O(d) where d is nesting depth.
Flatten with Generator
Uses Python yield from for clean recursive flattening. The generator frame stores cursor state automatically.
def _gen(lst):
for item in lst:
yield from _gen(item) if isinstance(item, list) else [item]
Time: O(n) total. Space: O(d) call stack depth. Preferred for nested arbitrary structures when Python is available.
k-Way Merge via Min-Heap
Merges k sorted iterators. Push (first_value, i, iterator_i) for each iterator. Pop min, yield it, push the next from the same iterator.
heapq.heappush(heap, (val, i, it))
val, i, it = heapq.heappop(heap)
Time: O(n log k) for n total elements across k iterators. Space: O(k) heap.
Zigzag / Round-Robin Merge
Maintains a queue of iterators. On each next(), pop from the front, yield its next element, push back if not exhausted.
it = deque.popleft()
val = next(it)
if has_more(it): deque.append(it)
Time: O(1) per element. Space: O(k) queue.
BST Iterator (Controlled In-Order)
Maintains an explicit stack simulating the recursive call stack. hasNext() checks if stack is non-empty; next() pops and pushes right subtree's leftmost spine.
# push leftmost spine on init; in next():
node = stack.pop(); push_left(node.right)
Time: O(h) space for the stack; O(1) amortised per call (each node pushed/popped once).
See binary-search-tree.md for full BST traversal coverage.
RLE / Compressed Iterator
Track (current_value, remaining_count). next() decrements count; when count hits 0, advance to the next encoded segment.
Time: O(1) per next(). Space: O(1).
7. Problem Taxonomy
By Problem Category
| Category | What the problem asks | Key technique |
|---|---|---|
| Wrap existing iterator | Add peek() to an iterator that only has next()/hasNext() | One-element peek buffer |
| Flatten nested structure | Linearise a list of lists or NestedInteger into a flat sequence | Stack-based iterative DFS or generator |
| Merge sorted streams | Combine k sorted iterators into one sorted output | Min-heap k-way merge |
| Interleave streams | Alternate between multiple iterators without ordering | Round-robin deque |
| Compressed sequence | Decode RLE or encoded pairs into individual elements | Segment counter |
| Combinatorial iteration | Yield combinations/permutations one at a time | Index-based or backtracking with continuation |
| Tree traversal as iterator | Traverse a BST/tree node-by-node without full upfront traversal | Explicit stack with lazy spine-push |
Problem Signal → Technique
| Signal in problem statement | Technique |
|---|---|
"Implement peek()" | One-element peek buffer |
"Flatten nested list", NestedInteger interface | Stack-based flatten or yield from generator |
| "Merge k sorted lists/streams" | Min-heap with (value, iterator) tuples |
| "Zigzag", "alternate", "round-robin" | Deque of iterators, pop-yield-push |
| "Run-length encoding", "compressed" | Segment counter: (val, count) pair |
| "Next combination", "iterator for combinations" | Lexicographic index increment |
| "In-order iterator for BST" | Explicit stack, push leftmost spine lazily |
| "O(h) memory" in a tree problem | Iterative traversal with explicit stack, not full recursion |
"Design iterator", class with next()/hasNext() | Choose representation first (buffer / stack / heap) |
8. Common Patterns
| Pattern | When it applies | Mechanism |
|---|---|---|
| Peek buffer | Any design needing peek() or lookahead for merge decisions | Store one pre-fetched element; drain on next() |
Advance-in-hasNext() | Underlying source may produce invalid/empty elements that need skipping | Do the work of finding the next valid element in hasNext(); next() just returns it |
| Stack of iterators | Flattening nested or hierarchical structures | Push inner iterators onto a stack; exhaust top before moving to parent |
| Heap of iterator heads | Merging sorted streams | Heap entry = (value, tie-break, iterator); re-insert after each pop |
| Deque rotation | Round-robin over k live iterators | Pop front, produce element, push back if not exhausted |
| Generator delegation | Recursive or multi-level flattening in Python | yield from propagates to inner generators; frame is the cursor |
| Lazy spine push | Tree iterator with O(h) space | Push only the leftmost path; expand right child on pop |
9. Edge Cases & Pitfalls
-
Calling
next()withouthasNext(): Most iterator designs leave this undefined or raise an exception. Interviewers expect you to guard; always callhasNext()beforenext()in the driver loop. -
hasNext()has side effects: A common bug is placing cursor-advancing logic innext()only, makinghasNext()unreliable. Move the "find next valid element" logic intohasNext()so it is idempotent after the first call. -
Double-consuming the buffer: In a peek wrapper, forgetting to clear the buffer after
next()consumes it causes the same element to be returned twice. Always clear_peeked = Falsebefore returning. -
Empty iterator on
peek(): Callingpeek()on an exhausted iterator crashes unless guarded. Always checkhasNext()beforepeek(). -
Stack underflow in flatten: If the outer list is empty or all sublists are empty, the stack may be empty when
next()is called.hasNext()must returnFalsein this case;next()must never be called. -
Heap tie-breaking with non-comparable iterators:
heapqcompares tuple elements left to right. If two values are equal, Python will try to compare the iterator object, which raisesTypeError. Use a tie-break integer index:(val, idx, iterator). -
Mutation of the underlying collection during iteration: Modifying the list being iterated (appending, removing) while an iterator is active invalidates the cursor. Avoid; or snapshot the list on construction.
-
Off-by-one in RLE iterator: When
countreaches 0, the iterator must advance to the next segment before checkinghasNext(). Forgetting this yields one extra element or miscounts. -
Generator exhaustion is silent: A Python generator that returns
Nonewhen exhausted won't raiseStopIterationuntil the frame is done. Wrap withtry/except StopIterationor usenext(gen, sentinel)to detect exhaustion. -
BST iterator right-subtree push: After popping a node and yielding it, push the leftmost spine of
node.right, notnode.rightitself. Pushingnode.rightdirectly breaks in-order order.
10. Implementation Notes
-
Python
iter()andnext()builtins work with any object implementing__iter__and__next__; usenext(it, default)to avoidStopIterationexceptions when consuming unknown iterators. -
Python generators are the cleanest iterator implementation:
yieldsuspends the frame,yield fromdelegates to a sub-generator; the frame itself is the cursor and buffer. -
collections.dequeis the correct structure for round-robin rotation; listpop(0)is O(n). -
Python
heapqis a min-heap only; for max-heap semantics, negate values or use a wrapper class with__lt__defined. -
Recursion depth limit in Python is ~1000 by default; for deeply nested flatten problems, use the iterative stack approach to avoid
RecursionError. -
When implementing
hasNext()with a buffer-fill pattern, ensure the fill attempts exactly once per "slot" to avoid O(n) work per call on sparse iterators. -
In class-based iterator designs, do not store a reference to the original list and iterate by index — this breaks the lazy/streaming contract and wastes memory for large inputs.
11. Cross-Topic Relations
| Related topic | Connection |
|---|---|
| Stack | Flatten and BST iterator use an explicit stack to simulate recursion; the iterator IS a stack machine |
| Heap / Priority Queue | k-way merge iterator is built on a min-heap; the heap manages the merge invariant |
| Binary Search Tree | BST iterator (LeetCode 173) is a canonical iterator design problem; in-order traversal becomes an API |
| Linked List | Many iterator problems operate on linked list nodes; next-pointer manipulation is the same cursor pattern |
| Design patterns | Iterator is a structural design pattern; problems test encapsulation of traversal state |
| Generator / Coroutine | Python generators are first-class iterators; understanding generator protocol is prerequisite for clean solutions |
| Nested data structures | Flatten problems require understanding recursive list/tree structure; depth-first vs. breadth-first affects order |
| Two Pointers | Merge iterators and zigzag are two-pointer ideas lifted to the iterator abstraction level |
12. Interview Reference
Must-Know Problems
Foundational (understand the protocol)
- LeetCode 284 — Peeking Iterator (wrap with one-element buffer)
- LeetCode 173 — Binary Search Tree Iterator (lazy spine-push, O(h) space)
Flatten
- LeetCode 341 — Flatten Nested List Iterator (stack of iterators or generator)
- LeetCode 251 — Flatten 2D Vector (two-index cursor or iterator-of-iterators)
Merge / Interleave
- LeetCode 281 — Zigzag Iterator (round-robin deque, two iterators)
- LeetCode 23 — Merge k Sorted Lists (heap-based merge; lists are iterators here)
Compressed / Encoded
- LeetCode 900 — RLE Iterator (segment counter with skip logic)
- LeetCode 604 — Design Compressed String Iterator (character + count pair)
Combinatorial
- LeetCode 1286 — Iterator for Combination (lexicographic next-combination)
Clarification Questions to Ask
- "Should
next()throw on an exhausted iterator, or is the caller guaranteed to checkhasNext()first?" - "Is the nesting depth bounded, or can it be arbitrarily deep?" (affects stack vs. recursion choice)
- "Are the k input iterators guaranteed sorted, or just non-empty?" (affects whether heap is needed)
- "Should
peek()be idempotent — same value returned on repeated calls withoutnext()?" - "Is the iterator expected to handle an empty input on construction?"
- "For RLE: can count be zero in the encoded input?" (affects segment-skip logic)
Common Interview Mistakes
- Placing cursor-advancing logic only in
next(), causinghasNext()to be stateful and unreliable. - Forgetting to handle the case where all sublists/inner iterators are empty in a flatten design.
- Using a list index cursor instead of a proper iterator reference, losing the lazy evaluation property.
- Not using a tie-break index in heap entries, causing
TypeErrorwhen two values are equal. - In BST iterator, pushing
node.rightinstead of the leftmost spine ofnode.right. - Calling
next()without checkinghasNext()in driver code, causing an exception on empty input. - Making
peek()advance the cursor instead of buffering, so subsequent calls return different values.
Typical Follow-Up Escalations
- "Now support
prev()/ bidirectional iteration." — Requires a doubly-ended buffer or storing history; note this breaks the lazy property. - "Now merge k sorted iterators instead of 2." — Extend to a min-heap with
(value, index, iterator)tuples. - "What if the nested list has cycles?" — Add a visited set keyed on object identity; raise or skip on revisit.
- "Make your flatten iterator thread-safe." — Add a mutex around
hasNext()/next(); explain the TOCTOU issue between the two calls. - "Can you do this in O(1) space?" — For flatten: only possible if the nested structure provides parent pointers; otherwise O(d) is optimal.
- "Now skip the first n elements efficiently." — For RLE iterator: subtract n from segment counts without yielding; O(segments) not O(n).
Read Next
Start reading to get personalized recommendations
Explore Unread
Great job! You've read all available articles