1286. Iterator for Combination
Problem Description
You need to design a class called CombinationIterator
that generates all combinations of a given length from a string of characters in lexicographical order.
The class should support three operations:
-
Constructor
CombinationIterator(characters, combinationLength)
: Initialize the iterator with:characters
: a string of sorted, distinct lowercase English letterscombinationLength
: the desired length of each combination
-
next()
: Returns the next combination of the specified length in lexicographical order. Each call to this method should return a different combination until all combinations are exhausted. -
hasNext()
: Returnstrue
if there are more combinations available,false
otherwise.
For example, if you initialize the iterator with characters = "abc"
and combinationLength = 2
, the combinations would be "ab"
, "ac"
, "bc"
in that order. Calling next()
would return these combinations one by one, and hasNext()
would return true
until all combinations have been returned.
The solution uses a depth-first search (DFS) approach to generate all possible combinations during initialization. It explores two choices at each character position: either include the current character in the combination or skip it. When a combination reaches the desired length, it's added to the list. The class maintains an index to track which combination to return next, making the next()
and hasNext()
operations efficient with O(1)
time complexity.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: The problem doesn't involve nodes and edges or graph traversal. We're generating combinations from a string of characters.
Need to solve for kth smallest/largest?
- No: We're not finding a specific kth element. Instead, we need to generate all combinations in lexicographical order.
Involves Linked Lists?
- No: The problem works with strings and combinations, not linked list data structures.
Does the problem have small constraints?
- Yes: The problem involves generating combinations, which inherently has factorial-like growth. The number of combinations
C(n, k)
can quickly become large, but typically these problems have reasonable constraints since we need to enumerate all possibilities.
Brute force / Backtracking?
- Yes: We need to explore all possible combinations systematically. This is a classic backtracking scenario where we:
- Make a choice (include or exclude a character)
- Explore further with that choice
- Backtrack when we've either found a valid combination or exhausted possibilities
- Try the alternative choice
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This is the perfect pattern for generating all combinations because we need to systematically explore all possible selections of characters while maintaining the lexicographical order (which is naturally preserved when we process characters in their given sorted order).
Intuition
When we need to generate all combinations of a specific length from a set of characters, we face a classic selection problem: for each character, we must decide whether to include it or not in our current combination.
Think of it like building a team of k
players from n
available players. For each player, you make a binary decision - select them or skip them. This naturally leads to a recursive approach where we explore both possibilities at each step.
The key insight is that since our input characters are already sorted and we process them in order, the combinations will automatically be generated in lexicographical order. For example, with "abc"
and length 2, we'll naturally get "ab"
before "ac"
before "bc"
because we consider characters from left to right.
The backtracking pattern emerges from this decision tree:
- Start with an empty combination
- For each character position, we have two branches:
- Include the current character (add it to our temporary combination)
- Exclude the current character (skip to the next one)
- When our combination reaches the target length, we've found a valid result
- When we've processed all characters or can't possibly reach the target length, we backtrack
By pre-generating all combinations during initialization, we trade space for time - the next()
and hasNext()
operations become simple array lookups with O(1)
complexity. This is practical because the iterator pattern implies we'll likely access most or all combinations, making the upfront computation worthwhile.
The recursive DFS naturally handles the "backtracking" by using the call stack. When we append
a character, explore that path, then pop
it off, we're essentially undoing our choice and trying the alternative - this is the essence of backtracking.
Learn more about Backtracking patterns.
Solution Approach
The solution implements a backtracking algorithm to generate all combinations during initialization, then provides simple iteration through the pre-computed results.
Data Structure Design:
self.cs
: A list storing all valid combinations in lexicographical orderself.idx
: An index pointer tracking the current position for iterationt
: A temporary list used during DFS to build combinations character by character
The Core DFS Algorithm:
The dfs(i)
function explores all possible combinations starting from index i
:
-
Base Case - Valid Combination Found:
if len(t) == combinationLength: cs.append(''.join(t)) return
When our temporary list
t
reaches the target length, we've found a valid combination. We convert it to a string and store it. -
Base Case - Out of Bounds:
if i == n: return
If we've processed all characters without reaching the target length, we backtrack.
-
Recursive Exploration:
t.append(characters[i]) # Choose to include current character dfs(i + 1) # Explore with this choice t.pop() # Backtrack - undo the choice dfs(i + 1) # Explore without including current character
This is the heart of backtracking:
- First, we include
characters[i]
in our combination - Recursively explore all combinations that include this character
- Remove the character (backtrack) to restore the state
- Explore all combinations that exclude this character
- First, we include
Why This Generates Lexicographical Order:
Since the input characters are sorted and we process them left-to-right, always trying "include" before "exclude", we naturally generate combinations in lexicographical order. For "abc"
with length 2:
- First we try including 'a': leads to
"ab"
,"ac"
- Then we try excluding 'a' but including 'b': leads to
"bc"
Iterator Implementation:
Once all combinations are pre-computed:
next()
: Simply returnsself.cs[self.idx]
and increments the index -O(1)
operationhasNext()
: Checks ifself.idx < len(self.cs)
-O(1)
operation
Time and Space Complexity:
- Initialization:
O(C(n, k))
time and space, wheren
is the length of characters andk
is combinationLength next()
andhasNext()
:O(1)
time- Total space:
O(C(n, k))
to store all combinations
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with characters = "abc"
and combinationLength = 2
.
Initialization Phase - Building All Combinations:
We start DFS from index 0 with an empty temporary list t = []
:
DFS(0): t = [] ├─ Include 'a': t = ['a'] │ └─ DFS(1): t = ['a'] │ ├─ Include 'b': t = ['a','b'] │ │ └─ DFS(2): len(t) = 2 ✓ → Store "ab" │ └─ Exclude 'b': t = ['a'] │ └─ DFS(2): t = ['a'] │ ├─ Include 'c': t = ['a','c'] │ │ └─ DFS(3): len(t) = 2 ✓ → Store "ac" │ └─ Exclude 'c': t = ['a'] │ └─ DFS(3): i = 3 (out of bounds) → return └─ Exclude 'a': t = [] └─ DFS(1): t = [] ├─ Include 'b': t = ['b'] │ └─ DFS(2): t = ['b'] │ ├─ Include 'c': t = ['b','c'] │ │ └─ DFS(3): len(t) = 2 ✓ → Store "bc" │ └─ Exclude 'c': t = ['b'] │ └─ DFS(3): i = 3 (out of bounds) → return └─ Exclude 'b': t = [] └─ DFS(2): t = [] └─ Include 'c': t = ['c'] └─ DFS(3): len(t) = 1 < 2, i = 3 → return
After initialization:
self.cs = ["ab", "ac", "bc"]
self.idx = 0
Using the Iterator:
hasNext()
→ returnsTrue
(0 < 3)next()
→ returns"ab"
, increments idx to 1hasNext()
→ returnsTrue
(1 < 3)next()
→ returns"ac"
, increments idx to 2hasNext()
→ returnsTrue
(2 < 3)next()
→ returns"bc"
, increments idx to 3hasNext()
→ returnsFalse
(3 ≮ 3)
The backtracking ensures we explore all paths: when we add 'a' and find combinations "ab" and "ac", we backtrack (remove 'a') to explore paths without 'a', finding "bc". The lexicographical order is maintained because we always process characters left-to-right and try "include" before "exclude".
Solution Implementation
1class CombinationIterator:
2 def __init__(self, characters: str, combinationLength: int):
3 """
4 Initialize the iterator with all combinations of given length.
5
6 Args:
7 characters: A sorted string of distinct lowercase English letters
8 combinationLength: The length of combinations to generate
9 """
10 def generate_combinations(index: int) -> None:
11 """
12 Recursively generate all combinations using backtracking.
13
14 Args:
15 index: Current position in the characters string
16 """
17 # Base case: combination of desired length is formed
18 if len(current_combination) == combinationLength:
19 all_combinations.append(''.join(current_combination))
20 return
21
22 # Base case: reached end of characters string
23 if index == string_length:
24 return
25
26 # Include current character in combination
27 current_combination.append(characters[index])
28 generate_combinations(index + 1)
29
30 # Backtrack: exclude current character from combination
31 current_combination.pop()
32 generate_combinations(index + 1)
33
34 # Initialize variables for combination generation
35 all_combinations = []
36 string_length = len(characters)
37 current_combination = []
38
39 # Generate all combinations
40 generate_combinations(0)
41
42 # Store combinations and initialize iterator index
43 self.combinations = all_combinations
44 self.current_index = 0
45
46 def next(self) -> str:
47 """
48 Return the next combination in lexicographical order.
49
50 Returns:
51 The next combination string
52 """
53 result = self.combinations[self.current_index]
54 self.current_index += 1
55 return result
56
57 def hasNext(self) -> bool:
58 """
59 Check if there are more combinations available.
60
61 Returns:
62 True if more combinations exist, False otherwise
63 """
64 return self.current_index < len(self.combinations)
65
66
67# Your CombinationIterator object will be instantiated and called as such:
68# obj = CombinationIterator(characters, combinationLength)
69# param_1 = obj.next()
70# param_2 = obj.hasNext()
71
1class CombinationIterator {
2 // Total number of characters in the input string
3 private int totalCharacters;
4 // Required length of each combination
5 private int combinationLength;
6 // Input string containing characters
7 private String characters;
8 // StringBuilder to build current combination during DFS
9 private StringBuilder currentCombination = new StringBuilder();
10 // List to store all valid combinations in lexicographic order
11 private List<String> allCombinations = new ArrayList<>();
12 // Current index for iteration through combinations
13 private int currentIndex = 0;
14
15 /**
16 * Constructor initializes the iterator with given characters and combination length.
17 * Generates all combinations using DFS during initialization.
18 *
19 * @param characters Input string with unique sorted characters
20 * @param combinationLength Required length of each combination
21 */
22 public CombinationIterator(String characters, int combinationLength) {
23 this.totalCharacters = characters.length();
24 this.combinationLength = combinationLength;
25 this.characters = characters;
26 // Generate all combinations starting from index 0
27 generateCombinations(0);
28 }
29
30 /**
31 * Returns the next combination in lexicographic order.
32 *
33 * @return Next combination string
34 */
35 public String next() {
36 return allCombinations.get(currentIndex++);
37 }
38
39 /**
40 * Checks if there are more combinations available.
41 *
42 * @return true if more combinations exist, false otherwise
43 */
44 public boolean hasNext() {
45 return currentIndex < allCombinations.size();
46 }
47
48 /**
49 * DFS helper method to generate all combinations of required length.
50 * Uses backtracking to explore all possible combinations.
51 *
52 * @param startIndex Current position in the characters string
53 */
54 private void generateCombinations(int startIndex) {
55 // Base case: Found a valid combination of required length
56 if (currentCombination.length() == combinationLength) {
57 allCombinations.add(currentCombination.toString());
58 return;
59 }
60
61 // Base case: Reached end of string without forming complete combination
62 if (startIndex == totalCharacters) {
63 return;
64 }
65
66 // Choice 1: Include current character in the combination
67 currentCombination.append(characters.charAt(startIndex));
68 generateCombinations(startIndex + 1);
69
70 // Backtrack: Remove the last added character
71 currentCombination.deleteCharAt(currentCombination.length() - 1);
72
73 // Choice 2: Skip current character and move to next
74 generateCombinations(startIndex + 1);
75 }
76}
77
78/**
79 * Your CombinationIterator object will be instantiated and called as such:
80 * CombinationIterator obj = new CombinationIterator(characters, combinationLength);
81 * String param_1 = obj.next();
82 * boolean param_2 = obj.hasNext();
83 */
84
1class CombinationIterator {
2private:
3 string characters; // Input string of characters
4 vector<string> combinations; // All combinations of given length
5 int currentIndex; // Current position in combinations vector
6 int stringLength; // Length of input string
7 int combinationLength; // Required combination length
8 string currentCombination; // Temporary string for building combinations
9
10public:
11 /**
12 * Constructor: Generates all combinations of specified length from characters
13 * @param characters: Input string with unique characters in sorted order
14 * @param combinationLength: Length of each combination to generate
15 */
16 CombinationIterator(string characters, int combinationLength) {
17 this->currentIndex = 0;
18 this->stringLength = characters.size();
19 this->characters = characters;
20 this->combinationLength = combinationLength;
21
22 // Generate all combinations using DFS
23 generateCombinations(0);
24 }
25
26 /**
27 * Returns the next combination in lexicographical order
28 * @return: Next combination string
29 */
30 string next() {
31 return combinations[currentIndex++];
32 }
33
34 /**
35 * Checks if there are more combinations available
36 * @return: true if more combinations exist, false otherwise
37 */
38 bool hasNext() {
39 return currentIndex < combinations.size();
40 }
41
42private:
43 /**
44 * DFS helper function to generate all combinations
45 * @param startIndex: Current position in the characters string
46 */
47 void generateCombinations(int startIndex) {
48 // Base case: Found a valid combination
49 if (currentCombination.size() == combinationLength) {
50 combinations.push_back(currentCombination);
51 return;
52 }
53
54 // Base case: Reached end of string without forming complete combination
55 if (startIndex == stringLength) {
56 return;
57 }
58
59 // Choice 1: Include current character in combination
60 currentCombination.push_back(characters[startIndex]);
61 generateCombinations(startIndex + 1);
62 currentCombination.pop_back();
63
64 // Choice 2: Exclude current character from combination
65 generateCombinations(startIndex + 1);
66 }
67};
68
69/**
70 * Your CombinationIterator object will be instantiated and called as such:
71 * CombinationIterator* obj = new CombinationIterator(characters, combinationLength);
72 * string param_1 = obj->next();
73 * bool param_2 = obj->hasNext();
74 */
75
1// Global variables for managing combinations
2let characters: string = ""; // Input string of characters
3let combinations: string[] = []; // All combinations of given length
4let currentIndex: number = 0; // Current position in combinations array
5let stringLength: number = 0; // Length of input string
6let combinationLength: number = 0; // Required combination length
7let currentCombination: string = ""; // Temporary string for building combinations
8
9/**
10 * Initializes the combination iterator with given characters and combination length
11 * Generates all combinations of specified length from characters
12 * @param chars - Input string with unique characters in sorted order
13 * @param combLength - Length of each combination to generate
14 */
15function CombinationIterator(chars: string, combLength: number): void {
16 // Initialize global variables
17 currentIndex = 0;
18 stringLength = chars.length;
19 characters = chars;
20 combinationLength = combLength;
21 combinations = [];
22 currentCombination = "";
23
24 // Generate all combinations using DFS
25 generateCombinations(0);
26}
27
28/**
29 * Returns the next combination in lexicographical order
30 * @returns Next combination string
31 */
32function next(): string {
33 return combinations[currentIndex++];
34}
35
36/**
37 * Checks if there are more combinations available
38 * @returns true if more combinations exist, false otherwise
39 */
40function hasNext(): boolean {
41 return currentIndex < combinations.length;
42}
43
44/**
45 * DFS helper function to generate all combinations recursively
46 * @param startIndex - Current position in the characters string
47 */
48function generateCombinations(startIndex: number): void {
49 // Base case: Found a valid combination of required length
50 if (currentCombination.length === combinationLength) {
51 combinations.push(currentCombination);
52 return;
53 }
54
55 // Base case: Reached end of string without forming complete combination
56 if (startIndex === stringLength) {
57 return;
58 }
59
60 // Choice 1: Include current character in combination
61 currentCombination += characters[startIndex];
62 generateCombinations(startIndex + 1);
63 // Backtrack: Remove the last added character
64 currentCombination = currentCombination.slice(0, -1);
65
66 // Choice 2: Exclude current character from combination
67 generateCombinations(startIndex + 1);
68}
69
Time and Space Complexity
Time Complexity:
-
Initialization (
__init__
):O(C(n, k) * k)
wheren
is the length of the characters string andk
is the combinationLength. The DFS explores all possible combinations which isC(n, k) = n!/(k!(n-k)!)
, and for each valid combination, we joink
characters together which takesO(k)
time. -
next()
method:O(1)
- Simply returns the pre-computed combination at the current index and increments the index. -
hasNext()
method:O(1)
- Just compares two integers.
Space Complexity:
- Overall:
O(C(n, k) * k)
wheren
is the length of characters andk
is the combinationLength.- The
cs
list stores allC(n, k)
combinations, with each combination being a string of lengthk
. - The recursion stack depth is at most
O(n)
since we traverse through all character positions. - The temporary list
t
uses at mostO(k)
space. - Therefore, the dominant factor is the storage of all combinations:
O(C(n, k) * k)
.
- The
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Memory Overflow with Large Inputs
The most significant pitfall is pre-computing and storing all combinations during initialization. For large inputs, this can cause memory issues. For example, with 26 characters and combinationLength of 13, there are C(26,13) = 10,400,600 combinations to store.
Solution: Use lazy evaluation with bit manipulation or direct computation:
class CombinationIterator:
def __init__(self, characters: str, combinationLength: int):
self.characters = characters
self.combinationLength = combinationLength
self.current = [i for i in range(combinationLength)]
self.finished = False
def next(self) -> str:
result = ''.join(self.characters[i] for i in self.current)
# Generate next combination indices
n = len(self.characters)
for i in range(self.combinationLength - 1, -1, -1):
if self.current[i] < n - self.combinationLength + i:
self.current[i] += 1
for j in range(i + 1, self.combinationLength):
self.current[j] = self.current[j-1] + 1
break
else:
self.finished = True
return result
def hasNext(self) -> bool:
return not self.finished
2. Inefficient String Concatenation in Recursive Calls
Creating strings with ''.join(t)
during recursion for every valid combination can be inefficient, especially if done repeatedly.
Solution: Store indices instead of actual strings, then generate strings on-demand:
def generate_combinations(index: int) -> None:
if len(current_combination) == combinationLength:
# Store indices instead of the actual string
all_combinations.append(current_combination[:])
return
# ... rest of the logic
def next(self) -> str:
indices = self.combinations[self.current_index]
result = ''.join(self.characters[i] for i in indices)
self.current_index += 1
return result
3. Stack Overflow with Deep Recursion
For very long character strings with small combination lengths, the recursion depth could potentially exceed Python's default recursion limit (typically 1000).
Solution: Use iterative approach with explicit stack:
def generate_combinations_iterative(characters, combinationLength):
result = []
stack = [(0, [])]
while stack:
index, current = stack.pop()
if len(current) == combinationLength:
result.append(''.join(current))
continue
if index == len(characters):
continue
# Process in reverse order to maintain lexicographical order
stack.append((index + 1, current[:])) # Skip current character
stack.append((index + 1, current + [characters[index]])) # Include current
return result[::-1] # Reverse to get correct order
4. Not Handling Edge Cases
Failing to handle edge cases like empty strings or combinationLength greater than the string length.
Solution: Add validation in the constructor:
def __init__(self, characters: str, combinationLength: int):
if not characters or combinationLength <= 0:
self.combinations = []
self.current_index = 0
return
if combinationLength > len(characters):
self.combinations = []
self.current_index = 0
return
# ... rest of initialization
5. Incorrect Assumption About Input Order
The code assumes the input characters are already sorted. If they're not, the output won't be in lexicographical order.
Solution: Sort the characters explicitly:
def __init__(self, characters: str, combinationLength: int):
# Ensure characters are sorted for lexicographical order
characters = ''.join(sorted(characters))
# ... rest of initialization
How does merge sort divide the problem into subproblems?
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!