A wrapper iterator design that supports peeking at the next element without advancing the iterator.
Problem Restatement
We are given an existing iterator with these operations:
next()
hasNext()We need to design a new iterator called PeekingIterator that also supports:
peek()peek() should return the next element without moving the iterator forward.
That means calling:
peek()multiple times should always return the same value until:
next()is called.
The official statement defines peek() as retrieving the next element without advancing the iterator. The wrapped iterator already supports next() and hasNext(). (leetcode.com)
Input and Output
| Item | Meaning |
|---|---|
| Wrapped iterator | Existing iterator object |
peek() | Returns next value without consuming it |
next() | Returns next value and advances |
hasNext() | Returns whether more values exist |
Class shape:
class PeekingIterator:
def __init__(self, iterator):
...
def peek(self):
...
def next(self):
...
def hasNext(self):
...Examples
Suppose the underlying iterator contains:
[1, 2, 3]Calls behave like this:
| Call | Return | Iterator Position |
|---|---|---|
peek() | 1 | unchanged |
peek() | 1 | unchanged |
next() | 1 | moves forward |
next() | 2 | moves forward |
peek() | 3 | unchanged |
hasNext() | True | unchanged |
next() | 3 | moves forward |
hasNext() | False | unchanged |
The important behavior is:
peek()does not consume the element.
First Thought: Call next() Inside peek()
A naive idea is:
def peek(self):
return self.iterator.next()But this is incorrect.
Calling:
next()advances the iterator.
So after one peek(), the element would already be consumed.
Example:
peek() -> 1
next() -> 2This violates the required behavior.
We need a way to remember the next value before it is consumed.
Key Insight
Always cache the next element.
When the PeekingIterator is created:
- Read the first value from the underlying iterator.
- Store it in a variable called
next_value.
Now:
peek()simply returns the cached value.
When:
next()is called:
- Return the cached value.
- Advance the underlying iterator once.
- Update the cache.
So the cache always stores the next unread value.
Algorithm
During initialization:
If the underlying iterator has another value:
self.next_value = iterator.next()Otherwise:
self.next_value = NoneFor peek():
return self.next_valueFor next():
- Save the current cached value.
- Advance the wrapped iterator.
- Refresh the cache.
- Return the saved value.
For hasNext():
return self.next_value is not NoneCorrectness
At all times, next_value stores the next unread element from the underlying iterator, or None if no elements remain.
During initialization, the algorithm reads the first unread element and stores it in next_value. So the invariant holds initially.
peek() simply returns next_value without changing either the cache or the underlying iterator. Therefore the iterator position remains unchanged.
When next() is called, the algorithm returns the current cached value. This is exactly the next unread element.
Then the algorithm advances the wrapped iterator once and updates the cache to the following unread element, or None if no element remains.
So after every operation, next_value continues to store the next unread element.
Therefore:
peek()always returns the correct next element without consuming it.next()always returns the correct next element and advances exactly once.hasNext()correctly reports whether unread elements remain.
Complexity
| Operation | Time | Space | Why |
|---|---|---|---|
| Constructor | O(1) | O(1) | Stores one cached value |
peek() | O(1) | O(1) | Returns cached value |
next() | O(1) | O(1) | One iterator advance |
hasNext() | O(1) | O(1) | Cache existence check |
Implementation
class PeekingIterator:
def __init__(self, iterator):
self.iterator = iterator
if iterator.hasNext():
self.next_value = iterator.next()
else:
self.next_value = None
def peek(self):
return self.next_value
def next(self):
value = self.next_value
if self.iterator.hasNext():
self.next_value = self.iterator.next()
else:
self.next_value = None
return value
def hasNext(self):
return self.next_value is not NoneCode Explanation
We store the wrapped iterator:
self.iterator = iteratorThen we preload the first unread value:
if iterator.hasNext():
self.next_value = iterator.next()This creates the cache.
If no value exists:
self.next_value = NoneThe peek() method simply returns the cached value:
return self.next_valuewithout advancing anything.
In next():
value = self.next_valuestores the value we will return.
Then we refresh the cache:
if self.iterator.hasNext():
self.next_value = self.iterator.next()
else:
self.next_value = NoneFinally:
return valuereturns the old cached element.
hasNext() checks whether the cache still contains an unread value:
return self.next_value is not NoneAlternative Lazy Peek Design
Another design delays caching until the first peek() call.
This uses an additional flag:
self.has_peekedand only fetches the value when needed.
However, the eager preload version is simpler because:
peek()becomes trivial.hasNext()becomes simple.- State transitions are easier to reason about.
So the preload cache design is usually preferred.
Testing
class Iterator:
def __init__(self, nums):
self.nums = nums
self.index = 0
def hasNext(self):
return self.index < len(self.nums)
def next(self):
value = self.nums[self.index]
self.index += 1
return value
def test_peeking_iterator():
it = PeekingIterator(Iterator([1, 2, 3]))
assert it.peek() == 1
assert it.peek() == 1
assert it.next() == 1
assert it.next() == 2
assert it.peek() == 3
assert it.hasNext() is True
assert it.next() == 3
assert it.hasNext() is False
print("all tests passed")
test_peeking_iterator()Test meaning:
| Test | Why |
|---|---|
Multiple peek() calls | Confirms no advancement |
next() after peek() | Confirms correct value returned |
Final hasNext() | Confirms iterator exhaustion |
| Mixed operations | Confirms cache updates correctly |