A clear explanation of designing an iterator over a run-length encoded sequence without expanding it.
Problem Restatement
We need to design an iterator over a run-length encoded sequence.
The encoded array stores pairs:
[count, value]At every even index i, encoding[i] is the number of times encoding[i + 1] appears in the decoded sequence.
For example:
encoding = [3, 8, 0, 9, 2, 5]represents:
[8, 8, 8, 5, 5]because there are three 8s, zero 9s, and two 5s.
The iterator supports:
next(n)This consumes the next n elements and returns the last consumed element.
If there are fewer than n elements left, consume what remains and return -1.
LeetCode defines next(n) this way, with n >= 1, and guarantees at most 1000 calls per test case.
Input and Output
| Item | Meaning |
|---|---|
| Constructor input | encoding, the run-length encoded array |
next(n) input | Number of elements to consume |
next(n) output | Last exhausted value, or -1 |
| Encoding format | Even index is count, odd index is value |
| Count | May be zero |
| Decoded sequence | Should not be built explicitly |
Class shape:
class RLEIterator:
def __init__(self, encoding: list[int]):
...
def next(self, n: int) -> int:
...Examples
Example:
Input:
["RLEIterator","next","next","next","next"]
[[[3,8,0,9,2,5]],[2],[1],[1],[2]]
Output:
[null,8,8,5,-1]The encoded sequence is:
[8, 8, 8, 5, 5]Operations:
| Operation | Consumed | Return |
|---|---|---|
next(2) | 8, 8 | 8 |
next(1) | 8 | 8 |
next(1) | 5 | 5 |
next(2) | only one 5 remains | -1 |
The last call returns -1 because it asks for two elements, but only one element remains.
First Thought: Decode the Whole Sequence
A simple idea is to expand the encoded array into a normal list.
For:
[3, 8, 0, 9, 2, 5]we would build:
[8, 8, 8, 5, 5]Then next(n) just moves a pointer forward.
This is easy, but it can use too much memory because counts can be very large. The encoded array may be short while the decoded sequence is huge.
Key Insight
We only need to know the current run.
A run is one pair:
encoding[index] = remaining count
encoding[index + 1] = valueWhen next(n) is called, we consume from the current run.
If the current run has fewer than n elements, we skip the whole run and subtract its count from n.
If the current run has at least n elements, we subtract n from its count and return its value.
So the iterator can be represented by:
| Variable | Meaning |
|---|---|
encoding | The mutable encoded array |
index | The even index of the current run |
Algorithm
Constructor:
- Store
encoding. - Set
index = 0.
For next(n):
- While
indexis valid and the current count is smaller thann:- Subtract the current count from
n. - Move
indexto the next run by adding2.
- Subtract the current count from
- If
indexhas reached the end, return-1. - Otherwise, the current run has enough elements.
- Subtract
nfrom the current count. - Return the current value.
Walkthrough
Use:
encoding = [3, 8, 0, 9, 2, 5]Start:
index = 0
current run = 3 copies of 8Call next(2):
| Step | State |
|---|---|
Current count 3 >= 2 | Enough |
| Decrease count | 3 - 2 = 1 |
| Return | 8 |
Encoding is now logically:
[1, 8, 0, 9, 2, 5]Call next(1):
| Step | State |
|---|---|
Current count 1 >= 1 | Enough |
| Decrease count | 1 - 1 = 0 |
| Return | 8 |
Call next(1):
| Step | State |
|---|---|
Current count 0 < 1 | Skip run |
Next count 0 < 1 | Skip zero-count 9 run |
Next count 2 >= 1 | Enough |
| Decrease count | 2 - 1 = 1 |
| Return | 5 |
Call next(2):
| Step | State |
|---|---|
Current count 1 < 2 | Skip one 5 |
| No runs left | Return -1 |
Correctness
The iterator maintains the invariant that index points to the current run, and encoding[index] stores the number of unconsumed elements left in that run.
When next(n) is called, if the current run has fewer than n remaining elements, then all elements in that run must be consumed before the nth requested element can be reached. The algorithm subtracts that count from n and moves to the next run, preserving the meaning of n as the number of elements still needed.
If the algorithm reaches the end of the encoded array, then all remaining runs have been consumed and fewer than the requested n elements existed. Returning -1 is correct.
Otherwise, the current run contains at least n elements. The nth consumed element lies inside this run, so its value is encoding[index + 1]. The algorithm subtracts n from the remaining count and returns that value.
Thus, every call consumes exactly the requested number of elements when possible, returns the last consumed value, and returns -1 when not enough elements remain.
Complexity
Let:
m = len(encoding)| Operation | Time | Why |
|---|---|---|
| Constructor | O(1) | Stores the array and pointer |
next(n) | Amortized O(1) | Each run is skipped at most once over all calls |
Space complexity is O(1) extra space, not counting the stored input array.
Implementation
class RLEIterator:
def __init__(self, encoding: list[int]):
self.encoding = encoding
self.index = 0
def next(self, n: int) -> int:
while self.index < len(self.encoding) and self.encoding[self.index] < n:
n -= self.encoding[self.index]
self.index += 2
if self.index == len(self.encoding):
return -1
self.encoding[self.index] -= n
return self.encoding[self.index + 1]Code Explanation
The constructor stores the encoded representation:
self.encoding = encoding
self.index = 0The pointer index always points to a count position.
In next(n), we skip runs that do not have enough remaining elements:
while self.index < len(self.encoding) and self.encoding[self.index] < n:
n -= self.encoding[self.index]
self.index += 2If there are no runs left, the request cannot be satisfied:
if self.index == len(self.encoding):
return -1Otherwise, the current run contains the last consumed element:
self.encoding[self.index] -= n
return self.encoding[self.index + 1]We reduce the remaining count and return the run value.
Testing
def run_tests():
it = RLEIterator([3, 8, 0, 9, 2, 5])
assert it.next(2) == 8
assert it.next(1) == 8
assert it.next(1) == 5
assert it.next(2) == -1
it = RLEIterator([1, 10])
assert it.next(1) == 10
assert it.next(1) == -1
it = RLEIterator([0, 1, 0, 2, 3, 3])
assert it.next(2) == 3
assert it.next(1) == 3
assert it.next(1) == -1
it = RLEIterator([5, 7])
assert it.next(3) == 7
assert it.next(2) == 7
assert it.next(1) == -1
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
[3,8,0,9,2,5] | Official-style example with zero-count run |
[1,10] | Single run exhausted exactly |
| Leading zero-count runs | Skips empty runs correctly |
| Multiple calls inside one run | Maintains remaining count correctly |