Design the CombinationIterator
class:
CombinationIterator(string characters, int combinationLength)
Initializes the object with a string characters
of sorted distinct lowercase English letters and a number combinationLength
as arguments.next()
Returns the next combination of length combinationLength
in lexicographical order.hasNext()
Returns true
if and only if there exists a next combination.
Example 1:
Input ["CombinationIterator", "next", "hasNext", "next", "hasNext", "next", "hasNext"] [["abc", 2], [], [], [], [], [], []] Output [null, "ab", true, "ac", true, "bc", false] Explanation CombinationIterator itr = new CombinationIterator("abc", 2); itr.next(); // return "ab" itr.hasNext(); // return True itr.next(); // return "ac" itr.hasNext(); // return True itr.next(); // return "bc" itr.hasNext(); // return False
Constraints:
1 <= combinationLength <= characters.length <= 15
characters
are unique.104
calls will be made to next
and hasNext
.next
are valid.This problem requires designing a CombinationIterator
class that generates combinations of a given string. The solutions presented use two different approaches:
Solution 1: Backtracking
This approach uses a recursive backtracking algorithm to generate all possible combinations of length combinationLength
from the input string characters
. It then stores these combinations in a list and iterates through them using an index.
__init__(self, characters: str, combinationLength: int)
: The constructor initializes the iterator. It uses a depth-first search (DFS) function (dfs
) to generate all combinations. dfs
recursively adds characters to a temporary list (t
), adding the complete combination to cs
when the desired length is reached.
next(self) -> str
: This method returns the next combination from the pre-generated list cs
. It increments the index idx
after each call.
hasNext(self) -> bool
: This method checks if there are more combinations to return. It returns True
if idx
is less than the length of cs
, and False
otherwise.
Solution 2: Bit Manipulation
This solution leverages bit manipulation for a more concise and potentially efficient implementation.
__init__(self, characters: str, combinationLength: int)
: The constructor initializes the iterator. curr
is initialized to (1 << len(characters)) - 1
, representing the binary representation with all bits set (representing all characters selected). size
stores the desired combination length. cs
stores the reversed input string for easier bit manipulation.
next(self) -> str
: This method iterates backward from curr
, finding the next number with exactly size
bits set (representing the next combination). It constructs the combination string from the bits of curr
, reverses the string (as characters were stored in reverse order), and decrements curr
.
hasNext(self) -> bool
: This method similarly searches for the next valid curr
and returns True
if it exists, indicating more combinations are available, and False
otherwise.
Solution 1 (Backtracking):
Time Complexity: The time complexity is O(N * 2N), where N is the length of the input string characters
. This is because the backtracking algorithm explores all possible combinations. The generation of combinations takes exponential time.
Space Complexity: The space complexity is O(N * 2N) in the worst case to store all generated combinations in the cs
list.
Solution 2 (Bit Manipulation):
Time Complexity: The time complexity is O(N * C(N, K)), where N is the length of the input string and K is combinationLength
. The next()
and hasNext()
methods take time proportional to the number of combinations, which is given by C(N, K), the number of combinations of choosing K items out of N. The construction of a string takes O(K). Overall O(N*C(N,K)).
Space Complexity: The space complexity is O(N) to store the reversed input string. No extra space is used to generate combinations because it relies on bit manipulation.
Which solution is better?
Solution 2 (Bit manipulation) is generally preferred because it avoids explicitly generating and storing all combinations upfront, making it more memory-efficient for larger input strings. Solution 1 becomes very memory intensive as N grows because it generates all possible combinations first, storing them in memory. Solution 2 is generally faster as it avoids the overhead of recursion. However, the time complexity of both is impacted by the exponential nature of combinations. If only a few next()
calls are expected, the backtracking approach might be a simpler implementation.