1. Uncompressing the String (Time Limit Exceeded)

class StringIterator:
    def __init__(self, compressedString: str):
        self.res = []
        self.ptr = 0
        
        i = 0
        while i < len(compressedString):
            ch = compressedString[i]
            i += 1
            num = 0
            while i < len(compressedString) and compressedString[i].isdigit():
                num = num * 10 + int(compressedString[i])
                i += 1
            
            for j in range(num):
                self.res.append(ch)

    def next(self) -> str:
        if not self.hasNext():
            return ' '
        
        char = self.res[self.ptr]
        self.ptr += 1
        return char

    def hasNext(self) -> bool:
        return self.ptr != len(self.res)

Time & Space Complexity

  • We precompute the elements of the uncompressed string. Thus, the space required in this case is O(m)O(m), where mm refers to the length of the uncompressed string.

  • The time required for precomputation is O(m)O(m) since we need to generate the uncompressed string of length mm.

  • Once the precomputation has been done, the time required for performing next() and hasNext() is O(1)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)O(1) time. Operations like hasPrevious() can also be easily included.

  • Since once the precomputation has been done, next() requires O(1)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 mm is the length of the uncompressed string.


2. Pre-Computation

class StringIterator:
    def __init__(self, compressedString: str):
        self.ptr = 0
        import re
        self.nums = list(map(int, re.findall(r'\d+', compressedString)))
        self.chars = re.findall(r'[a-zA-Z]', compressedString)

    def next(self) -> str:
        if not self.hasNext():
            return ' '
        
        self.nums[self.ptr] -= 1
        res = self.chars[self.ptr]
        
        if self.nums[self.ptr] == 0:
            self.ptr += 1
        
        return res

    def hasNext(self) -> bool:
        return self.ptr != len(self.chars)

Time & Space Complexity

  • The space required for storing the results of the precomputation is O(n)O(n), where nn refers to the length of the compressed string. The numsnums and charschars array contain a total of nn elements.

  • The precomputation step requires O(n)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)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 nn is the length of the compressed string.


3. Demand-Computation

class StringIterator:
    
    def __init__(self, compressedString: str):
        self.res = compressedString
        self.ptr = 0
        self.num = 0
        self.ch = ' '
    
    def next(self) -> str:
        if not self.hasNext():
            return ' '
        
        if self.num == 0:
            self.ch = self.res[self.ptr]
            self.ptr += 1
            while self.ptr < len(self.res) and self.res[self.ptr].isdigit():
                self.num = self.num * 10 + int(self.res[self.ptr])
                self.ptr += 1

        self.num -= 1
        return self.ch
    
    def hasNext(self) -> bool:
        return self.ptr != len(self.res) or self.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)O(1).

  • The time required for hasNext() operation is O(1)O(1).

  • Since no precomputations are done, and hasNext() requires only O(1)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.