284. Peeking Iterator
Problem Description
This problem asks you to enhance a standard iterator with an additional peek
functionality. You need to design a PeekingIterator
class that wraps around an existing iterator and provides three operations:
-
next()
: Returns the next element in the sequence and advances the iterator to the following element. -
hasNext()
: Returnstrue
if there are more elements remaining in the sequence,false
otherwise. -
peek()
: Returns the next element in the sequence without advancing the iterator. This means callingpeek()
multiple times in a row should return the same element, and the iterator position should remain unchanged.
The key challenge is implementing peek()
since the underlying iterator only provides next()
(which advances) and hasNext()
operations. You need to figure out how to "look ahead" at the next element without consuming it from the original iterator.
For example, if you have an iterator over [1, 2, 3]
:
- Calling
peek()
returns1
but keeps the iterator at position1
- Calling
peek()
again still returns1
- Calling
next()
returns1
and advances to position2
- Calling
peek()
now returns2
without advancing - Calling
next()
returns2
and advances to position3
The solution uses a caching strategy: when peek()
is called, it fetches the next element from the underlying iterator using next()
and stores it in peeked_element
. A flag has_peeked
tracks whether we have a cached element. Subsequent calls to next()
return this cached element instead of calling the underlying iterator's next()
method.
Intuition
The main challenge is that the underlying iterator's next()
method is destructive - once you call it, the element is consumed and the iterator moves forward. But peek()
needs to show us the next element without consuming it.
Think of it like a queue of people where you can only see and remove the person at the front. If someone asks "who's next?" without wanting to remove them, you'd need to temporarily remove the person, remember who they are, and then handle them specially when someone actually wants to remove the next person.
This leads us to a caching approach: we need to "pre-fetch" the next element when peek()
is called. Since we've already consumed this element from the underlying iterator, we must store it somewhere until next()
is actually called.
The key insight is maintaining two pieces of state:
peeked_element
: Stores the pre-fetched element whenpeek()
is calledhas_peeked
: A boolean flag indicating whether we have a cached element
When peek()
is called, we check if we've already peeked. If not, we fetch the next element from the iterator and cache it. If we have already peeked, we just return the cached value.
When next()
is called, we need to consider two cases:
- If we have a cached element (from a previous
peek()
), return it and clear the cache - If we don't have a cached element, directly call the underlying iterator's
next()
For hasNext()
, we need to return true
if either we have a cached element OR the underlying iterator has more elements. This ensures consistency - if peek()
was successful, then hasNext()
should return true
.
This design elegantly handles all edge cases and maintains the iterator's expected behavior while adding the peek functionality with minimal overhead.
Solution Approach
The implementation uses a wrapper pattern with caching to add peek functionality to an existing iterator. Here's how each component works:
Data Structure:
self.iterator
: Stores reference to the underlying iteratorself.has_peeked
: Boolean flag indicating if we've cached an elementself.peeked_element
: Stores the cached element when peek is called
Constructor (__init__
):
def __init__(self, iterator):
self.iterator = iterator
self.has_peeked = False
self.peeked_element = None
Initializes the wrapper with the underlying iterator and sets up the caching mechanism with has_peeked
as False
initially.
peek()
Method:
def peek(self):
if not self.has_peeked:
self.peeked_element = self.iterator.next()
self.has_peeked = True
return self.peeked_element
- If we haven't peeked yet (
not self.has_peeked
), fetch the next element from the underlying iterator usingself.iterator.next()
- Cache this element in
self.peeked_element
and setself.has_peeked = True
- Return the cached element
- If we've already peeked, simply return the cached
self.peeked_element
next()
Method:
def next(self):
if not self.has_peeked:
return self.iterator.next()
result = self.peeked_element
self.has_peeked = False
self.peeked_element = None
return result
- If we haven't peeked (
not self.has_peeked
), directly returnself.iterator.next()
- If we have peeked, return the cached element and reset the cache by setting
self.has_peeked = False
andself.peeked_element = None
hasNext()
Method:
def hasNext(self):
return self.has_peeked or self.iterator.hasNext()
Returns True
if either:
- We have a cached element (
self.has_peeked
isTrue
), OR - The underlying iterator has more elements (
self.iterator.hasNext()
)
This implementation ensures that:
- Multiple consecutive
peek()
calls return the same value - A
next()
afterpeek()
returns the peeked value - The iterator behaves normally when
peek()
isn't used - Memory overhead is minimal - at most one element is cached at any time
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with the sequence [1, 2, 3]
:
Initial State:
iterator
points to[1, 2, 3]
has_peeked = False
peeked_element = None
Operation 1: peek()
- Since
has_peeked = False
, we calliterator.next()
which returns1
- Cache it:
peeked_element = 1
- Set
has_peeked = True
- Return
1
- State:
iterator
now points to[2, 3]
, but we have1
cached
Operation 2: peek()
again
- Since
has_peeked = True
, we don't call the iterator - Simply return
peeked_element = 1
- State remains unchanged
Operation 3: hasNext()
- Check:
has_peeked
isTrue
, so returnTrue
- (Even without checking
iterator.hasNext()
, we know we have an element)
Operation 4: next()
- Since
has_peeked = True
, we return the cachedpeeked_element = 1
- Clear cache:
has_peeked = False
,peeked_element = None
- Return
1
- State:
iterator
still points to[2, 3]
, cache is empty
Operation 5: next()
- Since
has_peeked = False
, directly calliterator.next()
- This returns
2
and advances the iterator - Return
2
- State:
iterator
points to[3]
, cache is still empty
Operation 6: peek()
- Since
has_peeked = False
, calliterator.next()
which returns3
- Cache it:
peeked_element = 3
,has_peeked = True
- Return
3
- State:
iterator
is now empty[]
, but we have3
cached
Operation 7: hasNext()
- Check:
has_peeked = True
, so returnTrue
- (The underlying iterator is empty, but we have a cached element)
Operation 8: next()
- Since
has_peeked = True
, return cachedpeeked_element = 3
- Clear cache:
has_peeked = False
,peeked_element = None
- Return
3
- State:
iterator
is empty[]
, cache is empty
Operation 9: hasNext()
- Check:
has_peeked = False
anditerator.hasNext() = False
- Return
False
(no more elements)
This walkthrough demonstrates how the caching mechanism allows us to "look ahead" without losing elements, maintaining the correct iterator behavior throughout.
Solution Implementation
1# Below is the interface for Iterator, which is already defined for you.
2#
3# class Iterator:
4# def __init__(self, nums: List[int]):
5# """
6# Initializes an iterator object to the beginning of a list.
7# """
8#
9# def hasNext(self) -> bool:
10# """
11# Returns true if the iteration has more elements.
12# """
13#
14# def next(self) -> int:
15# """
16# Returns the next element in the iteration.
17# """
18
19
20class PeekingIterator:
21 def __init__(self, iterator: Iterator):
22 """
23 Initialize the PeekingIterator with the given iterator.
24
25 Args:
26 iterator: The underlying iterator to wrap
27 """
28 self.iterator = iterator
29 self.has_peeked = False # Flag to track if we've peeked ahead
30 self.peeked_element = None # Cache for the peeked element
31
32 def peek(self) -> int:
33 """
34 Returns the next element in the iteration without advancing the iterator.
35
36 Returns:
37 The next element that would be returned by next()
38 """
39 # If we haven't peeked yet, fetch the next element and cache it
40 if not self.has_peeked:
41 self.peeked_element = self.iterator.next()
42 self.has_peeked = True
43
44 # Return the cached peeked element
45 return self.peeked_element
46
47 def next(self) -> int:
48 """
49 Returns the next element and advances the iterator.
50
51 Returns:
52 The next element in the iteration
53 """
54 # If we haven't peeked, simply return the next element from iterator
55 if not self.has_peeked:
56 return self.iterator.next()
57
58 # If we have peeked, return the cached element and reset the peek state
59 result = self.peeked_element
60 self.has_peeked = False
61 self.peeked_element = None
62 return result
63
64 def hasNext(self) -> bool:
65 """
66 Checks if there are more elements in the iteration.
67
68 Returns:
69 True if there are more elements, False otherwise
70 """
71 # We have next if either we've peeked (cached element exists)
72 # or the underlying iterator has more elements
73 return self.has_peeked or self.iterator.hasNext()
74
75
76# Your PeekingIterator object will be instantiated and called as such:
77# iter = PeekingIterator(Iterator(nums))
78# while iter.hasNext():
79# val = iter.peek() # Get the next element but not advance the iterator.
80# iter.next() # Should return the same value as [val].
81
1// Java Iterator interface reference:
2// https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html
3
4/**
5 * PeekingIterator extends the Iterator interface with a peek() operation.
6 * This allows viewing the next element without consuming it from the iterator.
7 */
8class PeekingIterator implements Iterator<Integer> {
9 // The underlying iterator that provides the actual elements
10 private Iterator<Integer> iterator;
11
12 // Flag to track whether we have a peeked element cached
13 private boolean hasPeeked;
14
15 // Cache for storing the peeked element
16 private Integer peekedElement;
17
18 /**
19 * Constructor initializes the PeekingIterator with a given iterator.
20 * @param iterator The underlying iterator to wrap
21 */
22 public PeekingIterator(Iterator<Integer> iterator) {
23 this.iterator = iterator;
24 this.hasPeeked = false;
25 this.peekedElement = null;
26 }
27
28 /**
29 * Returns the next element in the iteration without advancing the iterator.
30 * Multiple consecutive calls to peek() should return the same element.
31 * @return The next element that would be returned by next()
32 */
33 public Integer peek() {
34 // If we haven't peeked yet, fetch the next element and cache it
35 if (!hasPeeked) {
36 peekedElement = iterator.next();
37 hasPeeked = true;
38 }
39 // Return the cached peeked element
40 return peekedElement;
41 }
42
43 /**
44 * Returns the next element in the iteration and advances the iterator.
45 * @return The next element in the iteration
46 */
47 @Override
48 public Integer next() {
49 // If we haven't peeked, directly return from the underlying iterator
50 if (!hasPeeked) {
51 return iterator.next();
52 }
53
54 // If we have peeked, return the cached element and reset the peek state
55 Integer result = peekedElement;
56 hasPeeked = false;
57 peekedElement = null;
58 return result;
59 }
60
61 /**
62 * Returns true if the iteration has more elements.
63 * @return true if there are more elements, false otherwise
64 */
65 @Override
66 public boolean hasNext() {
67 // We have next if either we have a peeked element or the underlying iterator has next
68 return hasPeeked || iterator.hasNext();
69 }
70}
71
1/*
2 * Below is the interface for Iterator, which is already defined for you.
3 * **DO NOT** modify the interface for Iterator.
4 *
5 * class Iterator {
6 * struct Data;
7 * Data* data;
8 * public:
9 * Iterator(const vector<int>& nums);
10 * Iterator(const Iterator& iter);
11 *
12 * // Returns the next element in the iteration.
13 * int next();
14 *
15 * // Returns true if the iteration has more elements.
16 * bool hasNext() const;
17 * };
18 */
19
20class PeekingIterator : public Iterator {
21private:
22 bool has_peeked; // Flag to indicate if we have a cached peek value
23 int peeked_element; // Cached element from peek operation
24
25public:
26 /**
27 * Constructor initializes the PeekingIterator with a vector of integers.
28 * Inherits from base Iterator class and initializes peek state.
29 * @param nums - The input vector to iterate over
30 */
31 PeekingIterator(const vector<int>& nums) : Iterator(nums) {
32 // Initialize peek state - no element has been peeked yet
33 has_peeked = false;
34 }
35
36 /**
37 * Returns the next element in the iteration without advancing the iterator.
38 * Caches the peeked element for subsequent next() call.
39 * @return The next element that would be returned by next()
40 */
41 int peek() {
42 // If we haven't peeked yet, fetch and cache the next element
43 if (!has_peeked) {
44 peeked_element = Iterator::next();
45 has_peeked = true;
46 }
47 // Return the cached peeked element
48 return peeked_element;
49 }
50
51 /**
52 * Returns the next element and advances the iterator.
53 * If an element was peeked, returns the cached value instead.
54 * @return The next element in the iteration
55 */
56 int next() {
57 // If no element was peeked, directly return next from base iterator
58 if (!has_peeked) {
59 return Iterator::next();
60 }
61
62 // Return the cached peeked element and reset peek state
63 has_peeked = false;
64 return peeked_element;
65 }
66
67 /**
68 * Checks if the iterator has more elements.
69 * @return true if there are more elements, false otherwise
70 */
71 bool hasNext() const {
72 // Has next if either we have a peeked element or base iterator has more
73 return has_peeked || Iterator::hasNext();
74 }
75};
76
1// Flag to indicate if we have a cached peek value
2let hasPeeked: boolean = false;
3
4// Cached element from peek operation
5let peekedElement: number = 0;
6
7// Reference to the base iterator
8let baseIterator: Iterator;
9
10/**
11 * Initializes the PeekingIterator with an iterator.
12 * Sets up the peek state for managing cached values.
13 * @param iterator - The base iterator to wrap with peeking functionality
14 */
15function initialize(iterator: Iterator): void {
16 baseIterator = iterator;
17 hasPeeked = false;
18}
19
20/**
21 * Returns the next element in the iteration without advancing the iterator.
22 * Caches the peeked element for subsequent next() call.
23 * @returns The next element that would be returned by next()
24 */
25function peek(): number {
26 // If we haven't peeked yet, fetch and cache the next element
27 if (!hasPeeked) {
28 peekedElement = baseIterator.next();
29 hasPeeked = true;
30 }
31 // Return the cached peeked element
32 return peekedElement;
33}
34
35/**
36 * Returns the next element and advances the iterator.
37 * If an element was peeked, returns the cached value instead.
38 * @returns The next element in the iteration
39 */
40function next(): number {
41 // If no element was peeked, directly return next from base iterator
42 if (!hasPeeked) {
43 return baseIterator.next();
44 }
45
46 // Return the cached peeked element and reset peek state
47 hasPeeked = false;
48 return peekedElement;
49}
50
51/**
52 * Checks if the iterator has more elements.
53 * @returns true if there are more elements, false otherwise
54 */
55function hasNext(): boolean {
56 // Has next if either we have a peeked element or base iterator has more
57 return hasPeeked || baseIterator.hasNext();
58}
59
Time and Space Complexity
Time Complexity:
__init__()
:O(1)
- Simply initializes instance variables without any loops or recursive calls.peek()
:O(1)
- Either returns the already peeked element or callsiterator.next()
once and stores it. Both operations are constant time.next()
:O(1)
- Either returns the peeked element or callsiterator.next()
once. Both operations are constant time.hasNext()
:O(1)
- Performs a simple boolean check and callsiterator.hasNext()
which is assumed to beO(1)
.
Overall, all operations of the PeekingIterator have O(1)
time complexity.
Space Complexity:
O(1)
- The implementation uses only two additional instance variables:has_peeked
(a boolean) andpeeked_element
(stores at most one element from the iterator). The space usage doesn't scale with the input size, making it constant space complexity.
The solution efficiently implements a peeking iterator by caching at most one element when peek()
is called, allowing subsequent peek()
calls to return the same element without advancing the underlying iterator. This design maintains optimal time and space complexity for all operations.
Common Pitfalls
1. Not Handling Empty Iterator in peek()
A critical pitfall is calling peek()
when the iterator has no more elements. The current implementation will call self.iterator.next()
which could raise an exception or return undefined behavior on an empty iterator.
Problem Example:
iter = PeekingIterator(Iterator([])) # Empty iterator
val = iter.peek() # This will crash or throw an exception!
Solution:
Add a check before calling the underlying iterator's next()
:
def peek(self) -> int:
if not self.has_peeked:
if not self.iterator.hasNext():
raise StopIteration("No more elements to peek")
self.peeked_element = self.iterator.next()
self.has_peeked = True
return self.peeked_element
2. Thread Safety Issues
In multi-threaded environments, the caching mechanism isn't thread-safe. If multiple threads access the same PeekingIterator
instance, race conditions can occur between checking has_peeked
and modifying it.
Problem Scenario:
- Thread A calls
peek()
and setshas_peeked = True
- Thread B calls
next()
simultaneously and resetshas_peeked = False
- This leads to inconsistent state and incorrect behavior
Solution: Use thread synchronization:
import threading
class PeekingIterator:
def __init__(self, iterator: Iterator):
self.iterator = iterator
self.has_peeked = False
self.peeked_element = None
self.lock = threading.Lock()
def peek(self) -> int:
with self.lock:
if not self.has_peeked:
self.peeked_element = self.iterator.next()
self.has_peeked = True
return self.peeked_element
def next(self) -> int:
with self.lock:
if not self.has_peeked:
return self.iterator.next()
result = self.peeked_element
self.has_peeked = False
self.peeked_element = None
return result
3. Forgetting to Reset Cache State
A subtle bug can occur if you forget to properly reset both has_peeked
and peeked_element
in the next()
method. Setting only one but not the other can lead to memory leaks or incorrect values being returned.
Incorrect Implementation:
def next(self):
if not self.has_peeked:
return self.iterator.next()
result = self.peeked_element
self.has_peeked = False
# Forgot to set self.peeked_element = None
return result
This keeps a reference to the old element unnecessarily, which could be problematic for large objects.
4. Incorrect hasNext()
Logic with Short-Circuit Evaluation
The order of conditions in hasNext()
matters for efficiency. The current implementation correctly checks self.has_peeked
first (which is O(1)) before calling self.iterator.hasNext()
(which might be more expensive).
Less Efficient (but still correct):
def hasNext(self) -> bool:
return self.iterator.hasNext() or self.has_peeked # Calls hasNext() unnecessarily
The current implementation leverages short-circuit evaluation properly - if self.has_peeked
is True
, it doesn't need to check the underlying iterator at all.
Which of the following is a min heap?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!