LeetCode 284: Peeking Iterator Solution in Python – A Step-by-Step Guide
Imagine you’re flipping through a stack of cards one by one, but sometimes you want to sneak a peek at the next card without taking it yet—like a little preview before you commit. That’s the idea behind LeetCode 284: Peeking Iterator, a medium-level problem where you build a custom iterator that adds a peek()
method to a standard iterator, letting you see the next element without advancing. Using Python, we’ll explore two solutions: the Best Solution, a caching approach with a buffer that’s efficient and elegant, and an Alternative Solution, a method using a full copy of the iterator that’s simpler but less optimal. With detailed examples, clear code, and a friendly tone—especially for the caching breakdown—this guide will help you master this iterator, whether you’re new to coding or leveling up. Let’s peek into it!
What Is LeetCode 284: Peeking Iterator?
In LeetCode 284: Peeking Iterator, you’re given an existing Iterator
class with two methods: next()
(returns the next element and moves forward) and hasNext()
(checks if there’s more to come). Your task is to create a PeekingIterator
class that adds a peek()
method to look at the next element without advancing the iterator, while keeping next()
and hasNext()
working as expected. For example, with an array [1, 2, 3], you could call peek()
to see 1, then next()
to get 1, peek()
to see 2, and so on. This problem tests your ability to extend functionality, sharing a vibe with iterator challenges like LeetCode 281: Zigzag Iterator.
Problem Statement
- Input: An Iterator object wrapping an integer array.
- Output: A PeekingIterator class with:
- peek(): Returns the next element without advancing.
- next(): Returns the next element and advances.
- hasNext(): Returns True if more elements remain.
- Rules: Use the given Iterator; don’t assume array access.
Constraints
- Array length: 0 to 10³ (1,000).
- Element values: -10⁹ to 10⁹.
- Method calls: Up to 10⁴.
Examples
- Input: [1, 2, 3]
- Calls: peek() → 1, peek() → 1, next() → 1, next() → 2, peek() → 3, next() → 3, hasNext() → False
- Input: []
- Calls: hasNext() → False
- Input: [4]
- Calls: peek() → 4, next() → 4, hasNext() → False
Understanding the Problem: Adding a Sneak Peek
To solve LeetCode 284: Peeking Iterator in Python, we need to wrap the given Iterator
and add peek()
functionality, which previews the next element without moving forward, while ensuring next()
and hasNext()
still work seamlessly. We can’t directly access the underlying array—only the Iterator
’s methods—so we need a way to “look ahead” without losing our place. We’ll use:
1. Best Solution (Caching with Buffer): O(1) time per call, O(1) space—smart and efficient.
2. Alternative Solution (Iterator Copy): O(n) space—easier but heavier.
Let’s dive into the caching solution with a beginner-friendly explanation.
Best Solution: Caching with Buffer
Why This Is the Best Solution
The caching approach is the winner for LeetCode 284 because it’s fast—O(1) time for all methods—and lean, using O(1) space with just one extra variable. It stores the next element in a buffer when we peek, reusing it for next()
, then fetches a new one from the iterator. For beginners, it’s like keeping a sticky note of the next card—peek at it as much as you want, then grab it when ready!
How It Works
Think of this as a little lookahead trick: we keep a buffer (a single slot) that holds the next element we’ve peeked at. When we peek, we fill the buffer if it’s empty; when we advance, we use the buffer and fetch the next one. Here’s the step-by-step, explained simply:
- Step 1: Setup:
- Store the given Iterator.
- Add a buffer variable (initially None) and a has_buffer flag (False).
- Step 2: peek():
- If the buffer’s empty (has_buffer is False), fetch the next element from the iterator and store it.
- Return the buffer value—don’t move anything yet.
- Step 3: next():
- If the buffer has a value, use it, clear the buffer, and fetch the next one if available.
- If no buffer, just call iterator.next() directly.
- Return the value and update the state.
- Step 4: hasNext():
- Return True if there’s a buffer value or the iterator has more elements.
- Step 5: Why It Works:
- Buffer acts as a preview—peek sees it, next uses it.
- Only fetches from the iterator when needed, keeping it in sync.
- No need to store the whole array—just one lookahead.
It’s like peeking through a curtain without stepping forward!
Step-by-Step Example
Example: [1, 2, 3]
- Init: buffer = None, has_buffer = False, iterator at 1.
- peek(): No buffer—fetch 1, buffer = 1, has_buffer = True, return 1.
- peek(): Buffer has 1, return 1 (no change).
- next(): Use buffer 1, buffer = 2 (fetch next), has_buffer = True, return 1.
- next(): Use buffer 2, buffer = 3, return 2.
- peek(): Buffer has 3, return 3.
- next(): Use buffer 3, buffer = None, has_buffer = False, return 3.
- hasNext(): No buffer, iterator empty → False.
Code with Detailed Line-by-Line Explanation
Here’s the Python code, explained for beginners:
# Below is the interface for Iterator, which is already defined:
# class Iterator:
# def __init__(self, nums):
# pass
# def hasNext(self) -> bool:
# pass
# def next(self) -> int:
# pass
class PeekingIterator:
def __init__(self, iterator):
# Step 1: Store iterator and initialize buffer
self.iterator = iterator
self.buffer = None
self.has_buffer = False
def peek(self) -> int:
# Step 2: Show next element without advancing
if not self.has_buffer:
self.buffer = self.iterator.next()
self.has_buffer = True
return self.buffer
def next(self) -> int:
# Step 3: Return next element and advance
if self.has_buffer:
val = self.buffer
self.has_buffer = False
if self.iterator.hasNext():
self.buffer = self.iterator.next()
self.has_buffer = True
return val
return self.iterator.next()
def hasNext(self) -> bool:
# Step 4: Check if more elements exist
return self.has_buffer or self.iterator.hasNext()
- Line 14-16: Store the iterator; buffer starts empty, has_buffer tracks it.
- Line 18-22: peek():
- If no buffer, fetch from iterator and store it.
- Return the buffered value.
- Line 24-32: next():
- If buffer exists, use it, clear it, and fetch next if available.
- If no buffer, get directly from iterator.
- Return the value.
- Line 34-35: hasNext(): True if buffer’s full or iterator has more.
- Time Complexity: O(1) per call—constant operations.
- Space Complexity: O(1)—just one buffer variable.
This solution is like a perfect sneak peek—fast and light!
Alternative Solution: Using an Iterator Copy
Why an Alternative Approach?
Copying the entire iterator into a list lets us peek by looking ahead in the copy, keeping the original iterator in sync. It’s O(n) space—less efficient—but simpler to understand, making it a good stepping stone for beginners before tackling the caching method.
How It Works
Imagine duplicating your card stack: you peek at the copy’s next card but advance the original only when you take it:
- Step 1: Convert the iterator to a list at initialization.
- Step 2: Use a pointer to track the current position.
- Step 3: peek() looks at the list’s next spot; next() advances the pointer.
It’s like having a cheat sheet of the whole deck!
Step-by-Step Example
Example: [1, 2, 3]
- Init: List = [1, 2, 3], pos = 0.
- peek(): pos=0, return 1.
- next(): pos=0, return 1, pos=1.
- peek(): pos=1, return 2.
- next(): pos=1, return 2, pos=2.
- hasNext(): pos=2, len=3 → True.
Code for Iterator Copy Approach
class PeekingIterator:
def __init__(self, iterator):
# Step 1: Copy iterator to list
self.nums = []
while iterator.hasNext():
self.nums.append(iterator.next())
self.pos = 0
def peek(self) -> int:
# Step 2: Look at next element
return self.nums[self.pos] if self.pos < len(self.nums) else None
def next(self) -> int:
# Step 3: Return and advance
val = self.nums[self.pos]
self.pos += 1
return val
def hasNext(self) -> bool:
# Step 4: Check remaining elements
return self.pos < len(self.nums)
- Time Complexity: O(1) per call, but O(n) initialization.
- Space Complexity: O(n)—stores the full list.
It’s straightforward but heavy.
Comparing the Two Solutions
- Caching (Best):
- Pros: O(1) time, O(1) space, no preprocessing.
- Cons: Slightly trickier logic.
- Copy (Alternative):
- Pros: Easy to follow, list-based.
- Cons: O(n) space, upfront cost.
Caching wins for efficiency.
Additional Examples and Edge Cases
- Empty: [] → hasNext() = False.
- Single: [1] → peek() = 1, next() = 1.
- Repeated Peeks: [1, 2] → peek() = 1, peek() = 1, next() = 1.
Both handle these well.
Complexity Breakdown
- Caching: Time O(1) per call, Space O(1).
- Copy: Time O(1) per call (O(n) init), Space O(n).
Caching is the lean champ.
Key Takeaways
- Caching: Buffer for the win—sleek!
- Copy: List it out—simple but bulky.
- Iterators: Peek without losing place.
- Python Tip: Classes and logic shine—see [Python Basics](/python/basics).
Final Thoughts: Peek Like a Pro
LeetCode 284: Peeking Iterator in Python is a cool twist on iteration. The caching solution keeps it tight and fast, while copying offers a beginner-friendly path. Want more? Try LeetCode 281: Zigzag Iterator or LeetCode 341: Flatten Nested List Iterator. Ready to peek? Head to Solve LeetCode 284 on LeetCode and sneak a look today!