RLE Iterator
MedPrompt
We can use run-length encoding (i.e., RLE) to encode a sequence of integers. In a run-length encoded array of even length encoding (0-indexed), for all even i, encoding[i] tells us the number of times that the non-negative integer value encoding[i + 1] is repeated in the sequence.
- For example, the sequence
arr = [8,8,8,5,5]can be encoded to beencoding = [3,8,2,5].encoding = [3,8,0,9,2,5]andencoding = [2,8,1,8,2,5]are also valid RLE ofarr.
Given a run-length encoded array, design an iterator that iterates through it.
Implement the RLEIterator class:
RLEIterator(int[] encoded)Initializes the object with the encoded arrayencoded.int next(int n)Exhausts the nextnelements and returns the last element exhausted in this way. If there is no element left to exhaust, return-1instead.
Example 1:
Input
["RLEIterator", "next", "next", "next", "next"]
[[[3, 8, 0, 9, 2, 5]], [2], [1], [1], [2]]
Output
[null, 8, 8, 5, -1]
Explanation
RLEIterator rLEIterator = new RLEIterator([3, 8, 0, 9, 2, 5]); // This maps to the sequence [8,8,8,5,5].
rLEIterator.next(2); // exhausts 2 terms of the sequence, returning 8. The remaining sequence is now [8, 5, 5].
rLEIterator.next(1); // exhausts 1 term of the sequence, returning 8. The remaining sequence is now [5, 5].
rLEIterator.next(1); // exhausts 1 term of the sequence, returning 5. The remaining sequence is now [5].
rLEIterator.next(2); // exhausts 2 terms, returning -1. This is because the first term exhausted was 5,
but the second term did not exist. Since the last term exhausted does not exist, we return -1.
Constraints:
2 <= encoding.length <= 1000encoding.lengthis even.0 <= encoding[i] <= 1091 <= n <= 109- At most
1000calls will be made tonext.
Approaches
2 approaches with complexity analysis and trade-offs.
This brute-force approach involves pre-processing the run-length encoded array by fully decompressing it into the actual sequence of numbers. The constructor builds a large list containing every number in the sequence. The next method then simply advances a pointer through this pre-built list to find the required element.
Algorithm
- Initialization: In the constructor, create a new list to hold the decompressed sequence and an index to track the current position.
- Decompression: Iterate through the
encodingarray. For each pair(count, value), addvalueto the listcounttimes. This happens once during initialization. next(n)Call:- Calculate the target position by adding
nto the current index. - Check if the target position is beyond the bounds of the decompressed list.
- If it is, there are not enough elements. Update the index to the end of the list and return
-1. - If there are enough elements, update the index to the target position and return the element at
index - 1.
- Calculate the target position by adding
Walkthrough
The RLEIterator class maintains the entire expanded sequence in a list (e.g., ArrayList in Java) and a pointer or index to the current position in this sequence.
Constructor (RLEIterator(int[] encoding)):
- It initializes an empty list.
- It then iterates through the input
encodingarray, taking elements two at a time. For each pair(count, value), it adds thevalueto the listcounttimes. - This process fully expands the run-length encoded data into a standard sequence, which is stored for later use.
- An index, say
cursor, is initialized to 0.
next(int n) Method:
- This method simulates moving
nsteps forward in the decompressed list. - It checks if moving
nsteps from the currentcursorposition will go past the end of the list (cursor + n > list.size()). - If it does, it means we cannot exhaust
nelements. The iterator exhausts all remaining elements, and as per the problem, returns-1. - Otherwise, it advances the
cursorbynand returns the element at the new position minus one (list.get(cursor - 1)), which is the last element exhausted.
import java.util.ArrayList;import java.util.List; class RLEIterator { private List<Long> decompressed; private int index; public RLEIterator(int[] encoding) { // Note: Using Long for counts to avoid overflow, though List size is limited by Integer.MAX_VALUE // This approach is fundamentally flawed by memory limits regardless. this.decompressed = new ArrayList<>(); this.index = 0; for (int i = 0; i < encoding.length; i += 2) { long count = encoding[i]; int value = encoding[i + 1]; for (long j = 0; j < count; j++) { // This loop is the source of the TLE/MLE this.decompressed.add((long)value); } } } public int next(int n) { if (this.index + n > this.decompressed.size()) { this.index = this.decompressed.size(); // Exhaust all remaining return -1; } this.index += n; return this.decompressed.get(this.index - 1).intValue(); }}Complexity
Time
O(S) for the constructor, where S is the total number of elements in the decompressed sequence. Each `next(n)` call is O(1). The high cost of the constructor makes this approach inefficient.
Space
O(S), where S is the total number of elements in the decompressed sequence (i.e., the sum of all counts in the `encoding` array). This can be extremely large.
Trade-offs
Pros
The logic is straightforward and easy to understand.
Once the initial decompression is done, each
next(n)call is very fast, operating in constant time.
Cons
The constructor's time complexity is proportional to the total number of elements in the sequence (
S), which can be enormous (1000 * 10^9), leading to a Time Limit Exceeded (TLE) error.The space complexity is also
O(S), which will almost certainly cause a Memory Limit Exceeded (MLE) error given the constraints.This approach is not feasible for the problem's constraints.
This optimal approach avoids the massive memory and time costs of decompression by working with the encoding array directly. It uses a pointer to keep track of the current position in the encoded data. When next(n) is called, it iterates through the encoded pairs, consuming elements on-the-fly until n elements have been exhausted.
Algorithm
- Initialization: Store the
encodingarray and an index pointer,ptr, initialized to 0. next(n)Call:- Loop as long as the pointer
ptris within the bounds of theencodingarray. - Let
countbeencoding[ptr]andvaluebeencoding[ptr + 1]. - Case 1:
n <= count(The current group has enough elements).- Decrease the group's count by
n:encoding[ptr] -= n. - The last element exhausted is
value, so return it.
- Decrease the group's count by
- Case 2:
n > count(The current group is not enough).- Exhaust the entire current group. Decrease
nbycount:n -= encoding[ptr]. - Move the pointer to the next group:
ptr += 2.
- Exhaust the entire current group. Decrease
- Loop as long as the pointer
- Termination: If the loop finishes (i.e.,
ptrgoes out of bounds) without returning a value, it means there were not enough elements in the entire sequence to satisfy thenext(n)call. Return-1.
Walkthrough
Instead of creating a new, potentially huge, data structure, this method cleverly uses the input encoding array itself to maintain the state of the iterator. It only needs one extra piece of information: an index or pointer to the current group (count, value) it is processing.
State:
int[] encoding: The run-length encoded array.int index: A pointer to the start of the current group in theencodingarray (always an even number).
Constructor (RLEIterator(int[] encoding)):
- It simply stores a reference to the
encodingarray and initializes theindexpointer to 0. This is a very fast O(1) operation.
next(int n) Method:
- The method enters a loop that continues as long as there are groups left to process (
index < encoding.length). - Inside the loop, it looks at the current group's count,
encoding[index]. - If
nis smaller than or equal to the current count, it means the group has enough elements to satisfy the request. The last element exhausted will be the value of this group,encoding[index + 1]. We subtractnfrom the count (encoding[index] -= n) to reflect the consumption and return the value. - If
nis larger than the current count, the entire group is consumed. We subtract the count fromn(n -= encoding[index]) and advance ourindexby 2 to move to the next group. - If the loop completes, it means we have traversed all the groups but still couldn't exhaust
nelements. In this case, we return-1.
class RLEIterator { private int[] encoding; private int index; public RLEIterator(int[] encoding) { this.encoding = encoding; this.index = 0; } public int next(int n) { while (index < encoding.length) { // If the current group has enough elements to satisfy the request if (n <= encoding[index]) { encoding[index] -= n; return encoding[index + 1]; } // Otherwise, the current group is not enough. // Exhaust the current group and move to the next. n -= encoding[index]; index += 2; } // If the loop finishes, we've run out of elements in the sequence. return -1; }}Complexity
Time
O(N) for all calls to `next` combined, where N is the length of the `encoding` array. The `index` pointer only moves forward, so it will traverse the array at most once across all calls. The constructor is O(1).
Space
O(1) extra space, as it only stores a pointer. It modifies the input array in-place.
Trade-offs
Pros
Extremely efficient in terms of space, using only O(1) extra space.
Efficient in time. The total work done across all
nextcalls is proportional to the length of theencodingarray, not the size of the decompressed sequence.It's the optimal solution for the given constraints.
Cons
This approach modifies the input
encodingarray. If the original array must be preserved, a copy should be made in the constructor, which would increase the space complexity to O(N), where N is the length of theencodingarray.
Solutions
Solution
class RLEIterator { private int [] encoding ; private int i ; private int j ; public RLEIterator ( int [] encoding ) { this . encoding = encoding ; } public int next ( int n ) { while ( i < encoding . length ) { if ( encoding [ i ] - j < n ) { n -= ( encoding [ i ] - j ); i += 2 ; j = 0 ; } else { j += n ; return encoding [ i + 1 ]; } } return - 1 ; } } /** * Your RLEIterator object will be instantiated and called as such: * RLEIterator obj = new RLEIterator(encoding); * int param_1 = obj.next(n); */class RLEIterator { public: RLEIterator ( vector < int >& encoding ) { this -> encoding = encoding ; } int next ( int n ) { while ( i < encoding . size ()) { if ( encoding [ i ] - j < n ) { n -= ( encoding [ i ] - j ); i += 2 ; j = 0 ; } else { j += n ; return encoding [ i + 1 ]; } } return - 1 ; } private: vector < int > encoding ; int i = 0 ; int j = 0 ; }; /** * Your RLEIterator object will be instantiated and called as such: * RLEIterator* obj = new RLEIterator(encoding); * int param_1 = obj->next(n); */class RLEIterator : def __init__ ( self , encoding : List [ int ]): self . encoding = encoding self . i = 0 self . j = 0 def next ( self , n : int ) -> int : while self . i < len ( self . encoding ): if self . encoding [ self . i ] - self . j < n : n -= self . encoding [ self . i ] - self . j self . i += 2 self . j = 0 else : self . j += n return self . encoding [ self . i + 1 ] return - 1 # Your RLEIterator object will be instantiated and called as such: # obj = RLEIterator(encoding) # param_1 = obj.next(n)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.