281. Zigzag Iterator 🔒
Problem Description
You need to design an iterator that takes two lists of integers and returns their elements in an alternating pattern (zigzag order).
The iterator should alternate between the two input vectors, returning one element from the first vector, then one from the second vector, then back to the first vector, and so on. When one vector is exhausted, the iterator should continue returning elements from the remaining vector.
The ZigzagIterator
class needs three methods:
-
Constructor
ZigzagIterator(v1, v2)
: Initializes the iterator with two integer listsv1
andv2
. -
hasNext()
: Returnstrue
if there are still elements to iterate through from either vector,false
if both vectors have been completely consumed. -
next()
: Returns the current element and advances the iterator to prepare for the next element. The elements should be returned in alternating order between the two vectors.
For example, if v1 = [1, 2]
and v2 = [3, 4, 5, 6]
, calling next()
repeatedly would return: 1, 3, 2, 4, 5, 6
.
The solution uses a circular approach with:
cur
: tracks which vector to read from next (0 or 1)indexes
: maintains the current position in each vectorvectors
: stores references to both input vectors
The hasNext()
method checks if there are remaining elements by cycling through vectors until it finds one with unread elements or determines all vectors are exhausted. The next()
method retrieves the current element, updates the index for that vector, and moves to the next vector in round-robin fashion.
Intuition
The key insight is to think of this problem as a round-robin scheduling scenario. When we need to alternate between two lists, we're essentially taking turns - first from list 1, then from list 2, then back to list 1, and so on.
To implement this "taking turns" behavior, we can use a pointer that cycles through the vectors in order. Think of it like dealing cards from multiple decks - you deal one card from deck 1, then one from deck 2, then back to deck 1. The dealer's hand moves in a circular pattern between the decks.
We need to track our position in each vector independently because they might have different lengths. This is why we maintain separate indexes for each vector - when we return to a vector after visiting the other one, we need to know where we left off.
The challenge comes when one vector runs out of elements before the other. In a naive alternating approach, we might try to access an element that doesn't exist. To handle this elegantly, when we encounter an exhausted vector during our round-robin cycle, we simply skip it and move to the next one. This is like a card dealer skipping an empty deck and continuing with the remaining decks.
The modulo operation (cur + 1) % size
gives us the circular behavior naturally - after vector 1 (index 1), we wrap back to vector 0. This pattern extends beautifully if we wanted to support more than two vectors.
For hasNext()
, we need to check if any vector still has elements. We start from our current position and cycle through all vectors. If we make a complete circle back to where we started without finding any remaining elements, we know the iteration is complete. This circular checking ensures we don't miss any remaining elements in vectors we haven't checked yet.
Solution Approach
The implementation uses a round-robin pattern with index tracking for each vector. Let's break down the key components and methods:
Data Structure Setup:
self.cur = 0
: Maintains which vector (0 or 1) we should read from nextself.size = 2
: The number of vectors we're alternating betweenself.indexes = [0] * self.size
: An array tracking the current reading position in each vector (initially all zeros)self.vectors = [v1, v2]
: Stores references to both input vectors for easy access
next()
Method Implementation:
- Get the current vector using
self.vectors[self.cur]
- Get the current index position for this vector using
self.indexes[self.cur]
- Retrieve the element at
vector[index]
and store it as the result - Increment the index for this vector:
self.indexes[self.cur] = index + 1
- Move to the next vector in round-robin fashion:
self.cur = (self.cur + 1) % self.size
- Return the retrieved element
The modulo operation ensures we cycle back to vector 0 after vector 1, creating the alternating pattern.
hasNext()
Method Implementation:
- Remember the starting position:
start = self.cur
- Enter a loop to find a vector with remaining elements:
- Check if the current vector is exhausted:
self.indexes[self.cur] == len(self.vectors[self.cur])
- If exhausted, move to the next vector:
self.cur = (self.cur + 1) % self.size
- If we've cycled through all vectors and returned to the start position, return
False
(no elements left)
- Check if the current vector is exhausted:
- If we find a vector with remaining elements, return
True
The clever part of hasNext()
is that it automatically positions self.cur
at the next valid vector. This means when next()
is called, it's guaranteed to find an element at the current position. This pre-positioning eliminates the need for additional checking in the next()
method.
Time Complexity:
next()
: O(1) - direct array access and simple arithmetic operationshasNext()
: O(k) where k is the number of vectors (in this case, k=2, so effectively O(1))
Space Complexity: O(1) additional space beyond storing the input vectors (we only maintain a few integer variables and references)
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through an example with v1 = [1, 2]
and v2 = [3, 4, 5]
.
Initial State:
cur = 0
(start with vector 0)indexes = [0, 0]
(both vectors start at position 0)vectors = [[1, 2], [3, 4, 5]]
Operation 1: hasNext()
- Check if
indexes[0] == len(vectors[0])
→0 == 2
? No - Return
True
(vector 0 has elements) cur
remains at 0
Operation 2: next()
- Access
vectors[0][indexes[0]]
→vectors[0][0]
= 1 - Update
indexes[0]
from 0 to 1 - Move
cur
to(0 + 1) % 2 = 1
- Return 1
Operation 3: next()
- Access
vectors[1][indexes[1]]
→vectors[1][0]
= 3 - Update
indexes[1]
from 0 to 1 - Move
cur
to(1 + 1) % 2 = 0
- Return 3
Operation 4: next()
- Access
vectors[0][indexes[0]]
→vectors[0][1]
= 2 - Update
indexes[0]
from 1 to 2 - Move
cur
to(0 + 1) % 2 = 1
- Return 2
Operation 5: hasNext()
- Start at
cur = 1
- Check if
indexes[1] == len(vectors[1])
→1 == 3
? No - Return
True
(vector 1 has elements) cur
remains at 1
Operation 6: next()
- Access
vectors[1][indexes[1]]
→vectors[1][1]
= 4 - Update
indexes[1]
from 1 to 2 - Move
cur
to(1 + 1) % 2 = 0
- Return 4
Operation 7: hasNext()
- Start at
cur = 0
- Check if
indexes[0] == len(vectors[0])
→2 == 2
? Yes (vector 0 exhausted) - Move
cur
to(0 + 1) % 2 = 1
- Check if
indexes[1] == len(vectors[1])
→2 == 3
? No - Return
True
(vector 1 still has elements) cur
stays at 1 (positioned at next valid vector)
Operation 8: next()
- Access
vectors[1][indexes[1]]
→vectors[1][2]
= 5 - Update
indexes[1]
from 2 to 3 - Move
cur
to(1 + 1) % 2 = 0
- Return 5
Operation 9: hasNext()
- Start at
cur = 0
- Check if
indexes[0] == len(vectors[0])
→2 == 2
? Yes (vector 0 exhausted) - Move
cur
to(0 + 1) % 2 = 1
- Check if
indexes[1] == len(vectors[1])
→3 == 3
? Yes (vector 1 exhausted) - Move
cur
to(1 + 1) % 2 = 0
- We've cycled back to start position 0, all vectors exhausted
- Return
False
Final output sequence: 1, 3, 2, 4, 5
The algorithm successfully alternates between vectors until one is exhausted, then continues with the remaining vector.
Solution Implementation
1from typing import List
2
3class ZigzagIterator:
4 def __init__(self, v1: List[int], v2: List[int]):
5 """
6 Initialize the zigzag iterator with two vectors.
7 The iterator will alternate between elements from v1 and v2.
8
9 Args:
10 v1: First vector of integers
11 v2: Second vector of integers
12 """
13 # Current vector index (0 or 1) to read from
14 self.current_vector_index = 0
15
16 # Number of vectors (always 2 in this implementation)
17 self.num_vectors = 2
18
19 # Track the current position/index within each vector
20 self.position_in_vectors = [0] * self.num_vectors
21
22 # Store references to both input vectors
23 self.vectors = [v1, v2]
24
25 def next(self) -> int:
26 """
27 Return the next element in zigzag order.
28 After returning an element from current vector, move to the next vector.
29
30 Returns:
31 The next integer in zigzag order
32 """
33 # Get the current vector and its current position
34 current_vector = self.vectors[self.current_vector_index]
35 current_position = self.position_in_vectors[self.current_vector_index]
36
37 # Retrieve the element at current position
38 result = current_vector[current_position]
39
40 # Increment the position for this vector
41 self.position_in_vectors[self.current_vector_index] = current_position + 1
42
43 # Move to the next vector in round-robin fashion
44 self.current_vector_index = (self.current_vector_index + 1) % self.num_vectors
45
46 return result
47
48 def hasNext(self) -> bool:
49 """
50 Check if there are remaining elements to iterate.
51 Skip empty/exhausted vectors to find the next available element.
52
53 Returns:
54 True if there are more elements, False otherwise
55 """
56 # Remember starting position to detect full cycle
57 start_index = self.current_vector_index
58
59 # Skip vectors that have been fully consumed
60 while self.position_in_vectors[self.current_vector_index] == len(self.vectors[self.current_vector_index]):
61 # Move to next vector
62 self.current_vector_index = (self.current_vector_index + 1) % self.num_vectors
63
64 # If we've checked all vectors and returned to start, no elements remain
65 if self.current_vector_index == start_index:
66 return False
67
68 return True
69
70
71# Your ZigzagIterator object will be instantiated and called as such:
72# i, v = ZigzagIterator(v1, v2), []
73# while i.hasNext(): v.append(i.next())
74
1public class ZigzagIterator {
2 // Current vector index for round-robin iteration
3 private int currentVectorIndex;
4 // Total number of vectors
5 private int totalVectors;
6 // List to track current position index in each vector
7 private List<Integer> positionIndexes = new ArrayList<>();
8 // List of input vectors to iterate through
9 private List<List<Integer>> vectors = new ArrayList<>();
10
11 /**
12 * Constructor initializes the iterator with two vectors
13 * @param v1 First vector
14 * @param v2 Second vector
15 */
16 public ZigzagIterator(List<Integer> v1, List<Integer> v2) {
17 currentVectorIndex = 0;
18 totalVectors = 2;
19 // Initialize position indexes for both vectors to 0
20 positionIndexes.add(0);
21 positionIndexes.add(0);
22 // Add both vectors to the list
23 vectors.add(v1);
24 vectors.add(v2);
25 }
26
27 /**
28 * Returns the next element in zigzag order
29 * @return Next element value
30 */
31 public int next() {
32 // Get the current vector and its position index
33 List<Integer> currentVector = vectors.get(currentVectorIndex);
34 int currentPosition = positionIndexes.get(currentVectorIndex);
35
36 // Retrieve the element at current position
37 int result = currentVector.get(currentPosition);
38
39 // Increment position index for current vector
40 positionIndexes.set(currentVectorIndex, currentPosition + 1);
41
42 // Move to next vector in round-robin fashion
43 currentVectorIndex = (currentVectorIndex + 1) % totalVectors;
44
45 return result;
46 }
47
48 /**
49 * Checks if there are more elements to iterate
50 * @return true if more elements exist, false otherwise
51 */
52 public boolean hasNext() {
53 int startingIndex = currentVectorIndex;
54
55 // Skip vectors that have been fully consumed
56 while (positionIndexes.get(currentVectorIndex) == vectors.get(currentVectorIndex).size()) {
57 // Move to next vector
58 currentVectorIndex = (currentVectorIndex + 1) % totalVectors;
59
60 // If we've cycled through all vectors without finding any elements, return false
61 if (startingIndex == currentVectorIndex) {
62 return false;
63 }
64 }
65
66 // Found a vector with remaining elements
67 return true;
68 }
69}
70
71/**
72 * Your ZigzagIterator object will be instantiated and called as such:
73 * ZigzagIterator i = new ZigzagIterator(v1, v2);
74 * while (i.hasNext()) v[f()] = i.next();
75 */
76
1class ZigzagIterator {
2private:
3 // Current vector index for round-robin iteration
4 int current_vector_index;
5 // Total number of vectors
6 int total_vectors;
7 // List to track current position index in each vector
8 vector<int> position_indexes;
9 // List of input vectors to iterate through
10 vector<vector<int>> vectors;
11
12public:
13 /**
14 * Constructor initializes the iterator with two vectors
15 * @param v1 First vector
16 * @param v2 Second vector
17 */
18 ZigzagIterator(vector<int>& v1, vector<int>& v2) {
19 current_vector_index = 0;
20 total_vectors = 2;
21 // Initialize position indexes for both vectors to 0
22 position_indexes.push_back(0);
23 position_indexes.push_back(0);
24 // Add both vectors to the list
25 vectors.push_back(v1);
26 vectors.push_back(v2);
27 }
28
29 /**
30 * Returns the next element in zigzag order
31 * @return Next element value
32 */
33 int next() {
34 // Get the current vector and its position index
35 vector<int>& current_vector = vectors[current_vector_index];
36 int current_position = position_indexes[current_vector_index];
37
38 // Retrieve the element at current position
39 int result = current_vector[current_position];
40
41 // Increment position index for current vector
42 position_indexes[current_vector_index] = current_position + 1;
43
44 // Move to next vector in round-robin fashion
45 current_vector_index = (current_vector_index + 1) % total_vectors;
46
47 return result;
48 }
49
50 /**
51 * Checks if there are more elements to iterate
52 * @return true if more elements exist, false otherwise
53 */
54 bool hasNext() {
55 int starting_index = current_vector_index;
56
57 // Skip vectors that have been fully consumed
58 while (position_indexes[current_vector_index] == vectors[current_vector_index].size()) {
59 // Move to next vector
60 current_vector_index = (current_vector_index + 1) % total_vectors;
61
62 // If we've cycled through all vectors without finding any elements, return false
63 if (starting_index == current_vector_index) {
64 return false;
65 }
66 }
67
68 // Found a vector with remaining elements
69 return true;
70 }
71};
72
73/**
74 * Your ZigzagIterator object will be instantiated and called as such:
75 * ZigzagIterator i(v1, v2);
76 * while (i.hasNext()) v[f()] = i.next();
77 */
78
1// Current vector index for round-robin iteration
2let currentVectorIndex: number;
3// Total number of vectors
4let totalVectors: number;
5// List to track current position index in each vector
6let positionIndexes: number[];
7// List of input vectors to iterate through
8let vectors: number[][];
9
10/**
11 * Constructor initializes the iterator with two vectors
12 * @param v1 - First vector
13 * @param v2 - Second vector
14 */
15function ZigzagIterator(v1: number[], v2: number[]): void {
16 currentVectorIndex = 0;
17 totalVectors = 2;
18 // Initialize position indexes for both vectors to 0
19 positionIndexes = [0, 0];
20 // Add both vectors to the list
21 vectors = [v1, v2];
22}
23
24/**
25 * Returns the next element in zigzag order
26 * @returns Next element value
27 */
28function next(): number {
29 // Get the current vector and its position index
30 const currentVector = vectors[currentVectorIndex];
31 const currentPosition = positionIndexes[currentVectorIndex];
32
33 // Retrieve the element at current position
34 const result = currentVector[currentPosition];
35
36 // Increment position index for current vector
37 positionIndexes[currentVectorIndex] = currentPosition + 1;
38
39 // Move to next vector in round-robin fashion
40 currentVectorIndex = (currentVectorIndex + 1) % totalVectors;
41
42 return result;
43}
44
45/**
46 * Checks if there are more elements to iterate
47 * @returns true if more elements exist, false otherwise
48 */
49function hasNext(): boolean {
50 const startingIndex = currentVectorIndex;
51
52 // Skip vectors that have been fully consumed
53 while (positionIndexes[currentVectorIndex] === vectors[currentVectorIndex].length) {
54 // Move to next vector
55 currentVectorIndex = (currentVectorIndex + 1) % totalVectors;
56
57 // If we've cycled through all vectors without finding any elements, return false
58 if (startingIndex === currentVectorIndex) {
59 return false;
60 }
61 }
62
63 // Found a vector with remaining elements
64 return true;
65}
66
67/**
68 * Your ZigzagIterator object will be instantiated and called as such:
69 * ZigzagIterator(v1, v2);
70 * while (hasNext()) v[f()] = next();
71 */
72
Time and Space Complexity
Time Complexity:
__init__
:O(1)
- The initialization only involves setting up a few variables and storing references to the input vectors. No iteration or copying is performed.next()
:O(1)
- This method performs constant-time operations: accessing an element from a vector by index, incrementing an index, and updating the current pointer using modulo operation.hasNext()
:O(k)
in the worst case, wherek
is the number of vectors (in this casek = 2
). The method may need to cycle through all vectors once to find a non-exhausted vector or determine that all vectors are exhausted. Sincek = 2
is constant here, this is effectivelyO(1)
.
Space Complexity:
-
O(1)
extra space - The iterator only maintains:self.cur
: a single integer for the current vector indexself.size
: a constant integer (always 2)self.indexes
: an array of size 2 to track current positionsself.vectors
: stores references to the input vectors (not copies)
Since the iterator stores only references to the original vectors rather than copying them, and maintains a fixed-size array of indices regardless of input size, the extra space used is constant
O(1)
. The input vectors themselves takeO(n + m)
space wheren
andm
are the lengths ofv1
andv2
, but this is not counted as extra space used by the iterator.
Common Pitfalls
1. Not Handling Empty Vectors Properly
A critical pitfall is failing to handle cases where one or both input vectors are empty from the start. If v1 = []
and v2 = [1, 2, 3]
, the iterator should seamlessly skip the empty vector and return elements from v2
.
Problem Example:
# Without proper handling, this could cause issues: v1 = [] v2 = [1, 2, 3] iterator = ZigzagIterator(v1, v2) # Calling next() might try to access v1[0] which doesn't exist
Solution:
The current implementation handles this correctly through the hasNext()
method which automatically skips exhausted (including initially empty) vectors. However, you should ensure hasNext()
is always called before next()
to guarantee safe operation.
2. Modifying self.cur
in hasNext()
Without Restoration
The hasNext()
method modifies the state by changing self.cur
to position it at the next valid vector. This is actually intentional design, but it can be confusing because typically a "check" method shouldn't modify state.
Problem Example:
# Calling hasNext() multiple times changes internal state
iterator = ZigzagIterator([1], [2])
print(iterator.hasNext()) # True, cur might be 0
print(iterator.hasNext()) # True, but cur might have changed
print(iterator.hasNext()) # True, cur keeps changing
Solution:
This is actually correct behavior for this implementation pattern. The key insight is that hasNext()
serves dual purpose:
- Checks if elements remain
- Positions the iterator at the next valid vector
Just be aware that calling hasNext()
multiple times without calling next()
is safe but may change internal positioning.
3. Infinite Loop in hasNext()
When All Vectors Are Empty
If both vectors are empty from the start, the while loop in hasNext()
could potentially create an infinite loop if not implemented carefully.
Problem Example:
v1 = [] v2 = [] iterator = ZigzagIterator(v1, v2) # The while loop needs proper termination condition
Solution: The implementation correctly handles this with the cycle detection:
if self.current_vector_index == start_index: return False
This ensures we only check each vector once and exit if we return to the starting position.
4. Assuming Equal-Length Vectors
A common mistake is writing code that assumes both vectors have the same length or that they'll exhaust simultaneously.
Problem Example:
# Wrong assumption: alternating until both are empty v1 = [1, 2] v2 = [3, 4, 5, 6, 7, 8] # Must continue with v2 after v1 is exhausted
Solution: The current implementation correctly handles unequal lengths by:
- Continuing to check both vectors in round-robin fashion
- Skipping exhausted vectors automatically
- Returning remaining elements from the non-exhausted vector
5. Not Preserving Round-Robin Order After Exhaustion
When one vector is exhausted, some implementations might stick to the remaining vector immediately. However, the correct behavior maintains the round-robin checking pattern.
Problem Example:
v1 = [1, 2, 3] v2 = [4] # After returning 1, 4, should return 2 (not immediately exhaust v1) # Correct order: 1, 4, 2, 3
Solution:
The modulo arithmetic (self.current_vector_index + 1) % self.num_vectors
ensures we always attempt to alternate, and hasNext()
skips over exhausted vectors while maintaining the pattern.
Which of these pictures shows the visit order of a depth-first search?
Recommended Readings
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
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
Want a Structured Path to Master System Design Too? Don’t Miss This!