Skip to content
← All posts

Peeking Iterator: Wrapper Pattern for Look-Ahead

5 min read
leetcodeproblemmediumdesign

LeetCode 284, Peeking Iterator, asks you to extend an existing iterator with a peek() method that returns the next element without advancing the iterator. You are given an Iterator class with next() and hasNext(). Your job is to wrap it in a PeekingIterator class that adds peek() while keeping next() and hasNext() working correctly.

The catch is that the underlying iterator only moves forward. Once you call next() on it, that element is gone. So how do you "look ahead" without losing the value? The answer is caching.

underlying iterator123next up45iterator poscached value3consumedpeekedremaining
After calling peek(), the next value (3) is cached. The underlying iterator has already advanced, but the cached value lets peek() return 3 again without consuming it.

The wrapper approach: cache the next value

The key insight is simple: when peek() is called, you call next() on the underlying iterator and store the result in a cache variable. If peek() is called again, you return the cached value without touching the underlying iterator. When next() is called, you check the cache first. If there is a cached value, return it and clear the cache. If there is no cached value, just forward the call to the underlying iterator.

This is the decorator/wrapper pattern. You are adding behavior (look-ahead) to an existing interface (iterator) without modifying the original class. The entire solution revolves around one extra variable: the cached value.

Here is the complete implementation:

class PeekingIterator:
    def __init__(self, iterator):
        self._iterator = iterator
        self._peeked = False
        self._peeked_value = None

    def peek(self):
        if not self._peeked:
            self._peeked_value = self._iterator.next()
            self._peeked = True
        return self._peeked_value

    def next(self):
        if self._peeked:
            self._peeked = False
            val = self._peeked_value
            self._peeked_value = None
            return val
        return self._iterator.next()

    def hasNext(self):
        return self._peeked or self._iterator.hasNext()

Step-by-step walkthrough

Let's trace through a sequence of operations on the iterator [1, 2, 3]: init, peek(), peek() again, next(), hasNext(), next(). Watch how the cache fills and empties as peek and next interact.

Step 1: init([1, 2, 3])

123poscachenonereturns-iterator: [1, 2, 3]

Constructor wraps the underlying iterator. No value is cached yet. The iterator points to the first element.

Step 2: peek() → 1

123poscache1returns1iterator: [1, 2, 3]

peek() calls next() on the underlying iterator to get 1, then stores it in the cache. The underlying iterator advances to position 1, but the cached value means we have not 'consumed' 1 yet.

Step 3: peek() → 1 (cached)

123poscache1returns1iterator: [1, 2, 3]

peek() again returns 1 from the cache without touching the underlying iterator. The cache makes repeated peeks O(1) with no side effects.

Step 4: next() → 1

123poscachenonereturns1iterator: [1, 2, 3]

next() returns the cached value 1 and clears the cache. Element 1 is now fully consumed. The underlying iterator is already at position 1, so no advancement is needed.

Step 5: hasNext() → True

123poscachenonereturnsTrueiterator: [1, 2, 3]

hasNext() checks: is there a cached value? No. Does the underlying iterator have more elements? Yes (element 2 is next). So it returns True.

Step 6: next() → 2

123poscachenonereturns2iterator: [1, 2, 3]

next() has no cached value, so it calls next() on the underlying iterator directly. Returns 2 and the iterator advances to position 2.

The important thing to notice is that peek() only advances the underlying iterator once. After the first peek, subsequent peeks return the cached value. And next() checks the cache before calling the underlying iterator, so no elements are ever skipped or duplicated.

Complexity analysis

OperationTimeSpace
peek()O(1)O(1)
next()O(1)O(1)
hasNext()O(1)O(1)
OverallO(n) for n operationsO(1) extra

Every operation is O(1). peek() calls next() on the underlying iterator at most once per element, and subsequent peeks return the cached value. next() either returns the cache or forwards to the underlying iterator. hasNext() checks a boolean flag and possibly the underlying iterator. The only extra space is one variable for the cached value, which is O(1).

The building blocks

Iterator wrapper / decorator pattern

The core technique here is wrapping an existing object to extend its behavior. You do not modify the Iterator class itself. Instead, you create a new class that holds a reference to the original iterator and intercepts its method calls. This is the decorator pattern from object-oriented design, and it shows up frequently in design problems. The same approach works for adding logging, filtering, or transformation to any iterator or stream.

Caching for look-ahead

The second building block is using a single cached value to provide look-ahead capability. This is a minimal buffer. You store exactly one element so that you can "peek" at it without losing it. The same idea appears in buffered readers, parser look-ahead tokens, and streaming algorithms where you need to inspect the next item before deciding what to do with the current one. The pattern is always the same: read ahead, store the value, and check the store before reading again.

Edge cases

  • Peek without ever calling next: if you call peek() multiple times and never call next(), the cache holds the same value and the underlying iterator stays in the same position. This is correct because peek should not consume the element.

  • Alternating peek and next: calling peek() then next() should return the same value both times. The first call caches it, the second returns and clears the cache. This is the most common source of bugs if you forget to clear the cache in next().

  • hasNext after all elements consumed: after the last next() call, hasNext() must return False. This works because the cache is empty and the underlying iterator has no more elements. Make sure your hasNext() checks both conditions with or, not and.

  • Single element iterator: with [42], calling peek() caches 42, then next() returns 42 and clears the cache. hasNext() returns False after that. This is a good minimal test to verify your cache logic handles the boundary correctly.

A common mistake is using a sentinel value like None to represent "no cached value." This breaks if None is a valid element in the iterator. Use a separate boolean flag (self._peeked) to track whether the cache is populated, independent of the cached value itself.

From understanding to recall

The peeking iterator is one of the cleanest examples of the wrapper pattern in LeetCode. The solution is short, but the interplay between the cache flag, the cached value, and the underlying iterator has subtle details. Do you clear the cache in next() or peek()? Do you check self._peeked or self._iterator.hasNext() or self._peeked and self._iterator.hasNext()? These are small decisions that are easy to get wrong under pressure.

Spaced repetition locks in these details. You write the PeekingIterator class from scratch after one day, then three days, then a week. Each time, you reconstruct the cache logic, the peek() guard, and the hasNext() check from memory. After a few rounds, the pattern is automatic. You stop thinking about whether to use or or and in hasNext() and just write it.

The wrapper pattern and the caching technique transfer directly to other design problems. Once you can write a peeking iterator from memory, you have a reusable template for any problem that asks you to add behavior to an existing interface.

Related posts