Facebook Pixel

284. Peeking Iterator

MediumDesignArrayIterator
Leetcode Link

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:

  1. next(): Returns the next element in the sequence and advances the iterator to the following element.

  2. hasNext(): Returns true if there are more elements remaining in the sequence, false otherwise.

  3. peek(): Returns the next element in the sequence without advancing the iterator. This means calling peek() 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() returns 1 but keeps the iterator at position 1
  • Calling peek() again still returns 1
  • Calling next() returns 1 and advances to position 2
  • Calling peek() now returns 2 without advancing
  • Calling next() returns 2 and advances to position 3

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. peeked_element: Stores the pre-fetched element when peek() is called
  2. has_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 iterator
  • self.has_peeked: Boolean flag indicating if we've cached an element
  • self.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 using self.iterator.next()
  • Cache this element in self.peeked_element and set self.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 return self.iterator.next()
  • If we have peeked, return the cached element and reset the cache by setting self.has_peeked = False and self.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 is True), OR
  • The underlying iterator has more elements (self.iterator.hasNext())

This implementation ensures that:

  1. Multiple consecutive peek() calls return the same value
  2. A next() after peek() returns the peeked value
  3. The iterator behaves normally when peek() isn't used
  4. 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 Evaluator

Example 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 call iterator.next() which returns 1
  • Cache it: peeked_element = 1
  • Set has_peeked = True
  • Return 1
  • State: iterator now points to [2, 3], but we have 1 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 is True, so return True
  • (Even without checking iterator.hasNext(), we know we have an element)

Operation 4: next()

  • Since has_peeked = True, we return the cached peeked_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 call iterator.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, call iterator.next() which returns 3
  • Cache it: peeked_element = 3, has_peeked = True
  • Return 3
  • State: iterator is now empty [], but we have 3 cached

Operation 7: hasNext()

  • Check: has_peeked = True, so return True
  • (The underlying iterator is empty, but we have a cached element)

Operation 8: next()

  • Since has_peeked = True, return cached peeked_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 and iterator.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 calls iterator.next() once and stores it. Both operations are constant time.
  • next(): O(1) - Either returns the peeked element or calls iterator.next() once. Both operations are constant time.
  • hasNext(): O(1) - Performs a simple boolean check and calls iterator.hasNext() which is assumed to be O(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) and peeked_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 sets has_peeked = True
  • Thread B calls next() simultaneously and resets has_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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following is a min heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More