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?

Section link icon

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

Section link icon

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

Section link icon

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

Section link icon

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

Section link icon
  • 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

Section link icon
  • 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

Section link icon
  • 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

Section link icon
  • 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

Section link icon

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!