Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free intro, Pro: 8 of 35 minutes free
Free: The problem: a menu of nested sections. With a pass: the other 11 sections and 3 interview answers, about 27 minutes.

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 as sections inside sections, with the items 101 to 105 numbered first to fifth in reading order.the menusectionempty sectionsection1033rdsection1011stsectionsectionsection1022ndempty section1044th1055th

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 copy-first iterator on deep nesting and on a caller who reads three items Python · brute/copy_first.py
"""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 from is 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.

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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.