Coding Interview Patterns Module 29 – Design-a-data-structure problems
Iterator design: flatten, peek, interleave
Design iterators that flatten a nested list lazily, peek ahead and interleave fairly: an original menu problem, the copy-first brute force, and the signal.
What you will learn
- Implement a lazy iterator over a nested list with a stack of positions, in O(depth) memory
- Prove that has_next() may be called again and again, and test the iterator against a flat copy
- Explain where recursive generators stop in CPython and Node.js, with measured depths
- Add peek() to any iterator with a sentinel that no stream can contain
- Design a fair round-robin iterator over several iterators, with a queue and O(1) work per item
Before you start
What you’ll learn · Before you start
What you will learn
- Implement a lazy iterator over a nested list with a stack of positions, in O(depth) memory
- Prove that has_next() may be called again and again, and test the iterator against a flat copy
- Explain where recursive generators stop in CPython and Node.js, with measured depths
- Add peek() to any iterator with a sentinel that no stream can contain
- Design a fair round-robin iterator over several iterators, with a queue and O(1) work per item
Before you start
On this page
A flatten nested list iterator hands out the items of a list whose elements are items or further lists, one item
per call, in reading order, without building a flat copy first. Two methods make up the class: next() returns the
next item and has_next() says whether one remains. Two related designs come with it. Peek shows the next item
without taking it, and interleave takes items from several iterators in turn. All three keep a little state between
calls instead of doing the work up front.
The problem: a menu of nested sections
The problem
A food-delivery app stores each restaurant’s menu as a nested list: an element is an item number or a section,
and a section is again a list of items and sections, possibly empty. Write MenuIterator(menu) with next(),
which returns the next item number in reading order, and has_next(), which says whether one remains and may be
called any number of times in a row. Limits: up to 10⁵ items and 10⁵ sections, nested up to 5,000 deep, and many
callers read only the first few items.
The menu [[101, [102]], [], [[[]]], 103, [[104, 105]]] gives 101, 102, 103, 104 and 105, after which has_next()
returns False. The empty sections hold nothing, but the iterator still has to step through them. The menu
[[], [[]]] gives no item at all: its first has_next() already returns False.
The example menu: sections inside sections, items numbered in reading order
Text description of the diagram
The outer box is the whole menu, [[101, [102]], [], [[[]]], 103, [[104, 105]]], with five elements stacked from top to bottom.
The first element is a section holding the item 101, numbered first, and an inner section holding 102, numbered second. The second element is an empty section. The third is a section inside a section inside a section, and the innermost one is empty, so all three hold no item. The fourth element is the item 103, numbered third. The fifth is a section holding another section, which holds 104 and 105, numbered fourth and fifth.
Reading order goes from top to bottom and, inside a section, from left to right, entering every section where it stands: 101, 102, 103, 104, 105. The empty sections add nothing to the order, but an iterator still has to step through them.
The brute force: copy every item first
The obvious design copies every item into a flat list in the constructor, recursing into each section, and then hands the items out with an index. Its answers are right, and it fails the limits in two ways:
"""The first design that comes to mind: copy every item into a flat list in the constructor, then hand them out with
an index. The answers are right; this program shows the two ways it fails at the stated sizes.
"""
class CopyFirstIterator:
def __init__(self, menu):
self.items = []
self.visited = 0 # elements the constructor looked at before next() could answer anything
self._copy(menu)
self.i = 0
def _copy(self, section):
for x in section:
self.visited += 1
if isinstance(x, list):
self._copy(x) # one Python frame per level of nesting
else:
self.items.append(x)
def has_next(self):
return self.i < len(self.items)
def next(self):
self.i += 1
return self.items[self.i - 1]
def nested(depth, item):
"""[[[...[item]...]]], `depth` lists deep."""
menu = [item]
for _ in range(depth - 1):
menu = [menu]
return menu
it = CopyFirstIterator([[101, [102]], [], [[[]]], 103, [[104, 105]]])
out = []
while it.has_next():
out.append(it.next())
print("example:", out)
# 1. Nesting: the copy recurses once per level, and CPython stops at its recursion limit of 1,000 frames.
for depth in (500, 990, 1_000, 5_000):
try:
it = CopyFirstIterator(nested(depth, 7))
print(f"depth {depth:,}: first item {it.next()}")
except RecursionError as e:
print(f"depth {depth:,}: RecursionError: {e}")
# 2. Work before the first answer: 100,000 items, each after an empty section, and a caller who reads 3 items.
menu = []
for k in range(100_000):
menu += [[], 1000 + k]
it = CopyFirstIterator(menu)
first = [it.next() for _ in range(3)]
print(f"200,000 elements, 3 items read {first}: the copy looked at {it.visited:,} elements "
f"and stored {len(it.items):,} items first") Output
example: [101, 102, 103, 104, 105] depth 500: first item 7 depth 990: first item 7 depth 1,000: RecursionError: maximum recursion depth exceeded depth 5,000: RecursionError: maximum recursion depth exceeded 200,000 elements, 3 items read [1000, 1001, 1002]: the copy looked at 200,000 elements and stored 100,000 items first
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 copy_first.py
The copy recurses once per level, and CPython stops it at depth 1,000, its default recursion limit, far short of
5,000. Even without recursion, it looks at all 200,000 elements and stores 100,000 items before it can
answer the first next(), for a caller who wanted three.
What to notice
An iterator only needs to remember where it is: in each open section, the position of the next element. That is a stack with one entry per level, so memory grows with the depth, not with the menu. Four designs follow:
- A stack of positions keeps one entry per open section and settles the next item only when asked.
- A recursive generator with
yield fromis short and lazy, but it nests one call per level. - A peeking wrapper caches one item, so that
peek()can show it without taking it. - Round-robin interleaving keeps a queue of iterators and takes one item from the front iterator per call.
A stack of positions, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Why has_next() may be called again and again, with a dry run
- Random menus against a flat copy
- Recursive generators, and the depth where they stop
- The code in five languages
- Peek: one item ahead, marked with a sentinel
- Interleaving fairly: a queue of iterators
- Edge cases and common bugs
- Exercise with sample tests
- A lazy iterator over a nested list
- A practice set with sample answers
- Check yourself
- Interview questions
- Why must an iterator's has_next() be safe to call twice in a row, and how do you make it so?
- Your recursive generator flattens nested lists and passes every sample test, but it crashes on a hidden test nested 5,000 deep. Why does it crash, and what do you change so that any depth works?
- Add peek() to any iterator, where the stream may contain None, 0, False and empty strings. What do you cache, how do you mark that nothing is cached, and what does each call cost?
- Mix k playlists so that each turn plays one song from the next playlist that still has songs. Some playlists are endless streams, and k can reach a million, most of them short. Design the iterator and give its cost per song.
- Key takeaways
8 runnable examples (C++, Go, Java, JavaScript, Python, check.py, menu_iterator.py) · 1 exercise with sample tests · 3 interview answers · about 23 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
References
- The Python Language Reference: The yield statement and yield expressions (Python Software Foundation)
- sys.getrecursionlimit (The Python Standard Library) (Python Software Foundation)
- Built-in functions: next, iter and zip (The Python Standard Library) (Python Software Foundation)
- itertools: zip_longest and islice (The Python Standard Library) (Python Software Foundation)
- collections.deque (The Python Standard Library) (Python Software Foundation)
- ECMAScript Language Specification: Generator Function Definitions and yield* (Ecma International (TC39))
- Java SE 25 API: Iterator (Oracle)
- The Java Language Specification, Java SE 25: 15.20.2 The instanceof Operator (Oracle)
- std::vector (cppreference.com) (cppreference.com)
- The Go Programming Language Specification: Appending to and copying slices (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress