Before attempting this problem, you should be comfortable with:
String Parsing - Extracting characters and numeric values from a formatted string
Iterator Pattern - Understanding how iterators maintain state between successive calls
Arrays/Lists - Using dynamic arrays to store precomputed results
1. Uncompressing the String (Time Limit Exceeded)
Intuition
The most straightforward approach is to fully decompress the string during initialization. We parse each character-count pair and append the character that many times to a result array. Then iteration simply walks through this precomputed array. This is easy to implement but can use excessive memory and time when counts are very large.
Algorithm
Parse the compressed string, extracting each character followed by its numeric count.
For each pair, append the character to a result array the specified number of times.
Maintain a pointer ptr starting at 0.
For next(): If hasNext() is false, return a space. Otherwise, return the character at ptr and increment ptr.
For hasNext(): Return true if ptr is less than the length of the result array.
classStringIterator:def__init__(self, compressedString:str):
self.res =[]
self.ptr =0
i =0while i <len(compressedString):
ch = compressedString[i]
i +=1
num =0while i <len(compressedString)and compressedString[i].isdigit():
num = num *10+int(compressedString[i])
i +=1for j inrange(num):
self.res.append(ch)defnext(self)->str:ifnot self.hasNext():return' '
char = self.res[self.ptr]
self.ptr +=1return char
defhasNext(self)->bool:return self.ptr !=len(self.res)
classStringIterator{StringBuilder res=newStringBuilder();int ptr=0;publicStringIterator(String s){int i =0;while(i < s.length()){char ch = s.charAt(i++);int num =0;while(i < s.length()&&Character.isDigit(s.charAt(i))){
num = num *10+ s.charAt(i)-'0';
i++;}for(int j =0; j < num; j++)
res.append(ch);}}publiccharnext(){if(!hasNext())return' ';return res.charAt(ptr++);}publicbooleanhasNext(){return ptr!=res.length();}}
classStringIterator{public:
string res;int ptr;StringIterator(string compressedString){
res ="";
ptr =0;int i =0;while(i < compressedString.length()){char ch = compressedString[i++];int num =0;while(i < compressedString.length()&&isdigit(compressedString[i])){
num = num *10+ compressedString[i]-'0';
i++;}for(int j =0; j < num; j++)
res += ch;}}charnext(){if(!hasNext())return' ';return res[ptr++];}boolhasNext(){return ptr != res.length();}};
publicclassStringIterator{privateList<char> res;privateint ptr;publicStringIterator(string compressedString){
res =newList<char>();
ptr =0;int i =0;while(i < compressedString.Length){char ch = compressedString[i++];int num =0;while(i < compressedString.Length &&char.IsDigit(compressedString[i])){
num = num *10+(compressedString[i]-'0');
i++;}for(int j =0; j < num; j++)
res.Add(ch);}}publiccharNext(){if(!HasNext())return' ';return res[ptr++];}publicboolHasNext(){return ptr != res.Count;}}
type StringIterator struct{
res []byte
ptr int}funcConstructor(compressedString string) StringIterator {
res :=[]byte{}
i :=0for i <len(compressedString){
ch := compressedString[i]
i++
num :=0for i <len(compressedString)&& compressedString[i]>='0'&& compressedString[i]<='9'{
num = num*10+int(compressedString[i]-'0')
i++}for j :=0; j < num; j++{
res =append(res, ch)}}return StringIterator{res: res, ptr:0}}func(this *StringIterator)Next()byte{if!this.HasNext(){return' '}
ch := this.res[this.ptr]
this.ptr++return ch
}func(this *StringIterator)HasNext()bool{return this.ptr !=len(this.res)}
classStringIterator(compressedString: String){privateval res =StringBuilder()privatevar ptr =0init{var i =0while(i < compressedString.length){val ch = compressedString[i++]var num =0while(i < compressedString.length && compressedString[i].isDigit()){
num = num *10+(compressedString[i]-'0')
i++}repeat(num){ res.append(ch)}}}funnext(): Char {if(!hasNext())return' 'return res[ptr++]}funhasNext(): Boolean {return ptr != res.length
}}
classStringIterator{privatevar res:[Character]privatevar ptr:Intinit(_ compressedString:String){
res =[]
ptr =0let chars =Array(compressedString)var i =0while i < chars.count {let ch = chars[i]
i +=1var num =0while i < chars.count && chars[i].isNumber {
num = num *10+Int(String(chars[i]))!
i +=1}for_in0..<num {
res.append(ch)}}}funcnext()->Character{if!hasNext(){return" "}let ch = res[ptr]
ptr +=1return ch
}funchasNext()->Bool{return ptr != res.count
}}
Time & Space Complexity
We precompute the elements of the uncompressed string. Thus, the space required in this case is O(m), where m refers to the length of the uncompressed string.
The time required for precomputation is O(m) since we need to generate the uncompressed string of length m.
Once the precomputation has been done, the time required for performing next() and hasNext() is O(1) for both.
This approach can be easily extended to include previous(), last() and find() operations. All these operations require the use of an index only and thus, take O(1) time. Operations like hasPrevious() can also be easily included.
Since once the precomputation has been done, next() requires O(1) time, this approach is useful if next() operation needs to be performed a large number of times. However, if hasNext() is performed most of the times, this approach isn't much advantageous since precomputation needs to be done anyhow.
A potential problem with this approach could arise if the length of the uncompressed string is very large. In such a case, the size of the complete uncompressed string could become so large that it can't fit in the memory limits, leading to memory overflow.
Where m is the length of the uncompressed string.
2. Pre-Computation
Intuition
Instead of fully decompressing, we can store the compressed representation more efficiently by separating characters and their counts into parallel arrays. During iteration, we track which character group we're in and how many of that character remain. When a count reaches zero, we move to the next group. This uses space proportional to the compressed string rather than the decompressed length.
Algorithm
Parse the compressed string using regex or manual parsing to extract two arrays: chars (the letters) and nums (the counts).
Maintain a pointer ptr to the current character group.
For next(): If hasNext() is false, return a space. Otherwise, decrement nums[ptr], get the character at chars[ptr], and if nums[ptr] becomes 0, increment ptr. Return the character.
For hasNext(): Return true if ptr is less than the length of chars.
classStringIterator{privatevar ptr =0privatevar chars:[Character]privatevar nums:[Int]init(_ compressedString:String){
chars =[]
nums =[]let charPattern =try!NSRegularExpression(pattern:"[a-zA-Z]")let numPattern =try!NSRegularExpression(pattern:"\\d+")let range =NSRange(compressedString.startIndex...,in: compressedString)let charMatches = charPattern.matches(in: compressedString, range: range)for match in charMatches {let matchRange =Range(match.range,in: compressedString)!
chars.append(compressedString[matchRange].first!)}let numMatches = numPattern.matches(in: compressedString, range: range)for match in numMatches {let matchRange =Range(match.range,in: compressedString)!
nums.append(Int(String(compressedString[matchRange]))!)}}funcnext()->Character{if!hasNext(){return" "}
nums[ptr]-=1let res = chars[ptr]if nums[ptr]==0{
ptr +=1}return res
}funchasNext()->Bool{return ptr != chars.count
}}
Time & Space Complexity
The space required for storing the results of the precomputation is O(n), where n refers to the length of the compressed string. The nums and chars array contain a total of n elements.
The precomputation step requires O(n) time. Thus, if hasNext() operation is performed most of the times, this precomputation turns out to be non-advantageous.
Once the precomputation has been done, hasNext() and next() require O(1) time.
This approach can be extended to include the previous() and hasPrevious() operations, but that would require making some simple modifications to the current implementation.
Where n is the length of the compressed string.
3. Demand-Computation
Intuition
The most space-efficient approach avoids any preprocessing. We store the original compressed string and parse it lazily as we iterate. We keep track of the current character and how many times it should still be returned. When that count reaches zero, we parse the next character-count pair from the string on demand.
Algorithm
Store the compressed string. Initialize ptr = 0, num = 0, and ch = ' '.
For next(): If hasNext() is false, return a space. If num is 0, parse the next character (store in ch) and its following digits (store in num). Decrement num and return ch.
For hasNext(): Return true if ptr is less than the string length OR num is greater than 0.
publicclassStringIterator{privatestring res;privateint ptr =0, num =0;privatechar ch =' ';publicStringIterator(string compressedString){
res = compressedString;}publiccharNext(){if(!HasNext())return' ';if(num ==0){
ch = res[ptr++];while(ptr < res.Length &&char.IsDigit(res[ptr])){
num = num *10+(res[ptr++]-'0');}}
num--;return ch;}publicboolHasNext(){return ptr != res.Length || num !=0;}}
type StringIterator struct{
res string
ptr int
num int
ch byte}funcConstructor(compressedString string) StringIterator {return StringIterator{res: compressedString, ptr:0, num:0, ch:' '}}func(this *StringIterator)Next()byte{if!this.HasNext(){return' '}if this.num ==0{
this.ch = this.res[this.ptr]
this.ptr++for this.ptr <len(this.res)&& this.res[this.ptr]>='0'&& this.res[this.ptr]<='9'{
this.num = this.num*10+int(this.res[this.ptr]-'0')
this.ptr++}}
this.num--return this.ch
}func(this *StringIterator)HasNext()bool{return this.ptr !=len(this.res)|| this.num !=0}
classStringIterator(compressedString: String){privateval res = compressedString
privatevar ptr =0privatevar num =0privatevar ch =' 'funnext(): Char {if(!hasNext())return' 'if(num ==0){
ch = res[ptr++]while(ptr < res.length && res[ptr].isDigit()){
num = num *10+(res[ptr++]-'0')}}
num--return ch
}funhasNext(): Boolean {return ptr != res.length || num !=0}}
classStringIterator{privatevar res:[Character]privatevar ptr =0privatevar num =0privatevar ch:Character=" "init(_ compressedString:String){
res =Array(compressedString)}funcnext()->Character{if!hasNext(){return" "}if num ==0{
ch = res[ptr]
ptr +=1while ptr < res.count && res[ptr].isNumber {
num = num *10+Int(String(res[ptr]))!
ptr +=1}}
num -=1return ch
}funchasNext()->Bool{return ptr != res.count || num !=0}}
Time & Space Complexity
Since no precomputation is done, constant space is required in this case.
The time required to perform next() operation is O(1).
The time required for hasNext() operation is O(1).
Since no precomputations are done, and hasNext() requires only O(1) time, this solution is advantageous if hasNext() operation is performed most of the times.
This approach can be extended to include previous() and hasPrevious() operations, but this will require the use of some additional variables.
Common Pitfalls
Not Handling Multi-Digit Counts
Counts can be more than one digit (e.g., "a12" means 12 a's). A common mistake is reading only a single digit.