1032. Stream of Characters
Problem Description
This problem asks you to design a data structure that can efficiently check if any suffix of a stream of characters matches any word from a given list of words.
You need to implement a StreamChecker
class with two methods:
-
Constructor
StreamChecker(String[] words)
: Initializes the stream checker with an array of words that you'll be checking against. -
Method
query(char letter)
: Takes a single character as input, adds it to the stream, and returnstrue
if any suffix of the current stream matches any word from the initial word list, otherwise returnsfalse
.
A suffix is a substring that ends at the last character of the string. For example, if the stream contains "axyz"
, the suffixes are: "z"
, "yz"
, "xyz"
, and "axyz"
.
Example walkthrough:
- Given
words = ["abc", "xyz"]
- Stream receives characters one by one:
'a'
,'x'
,'y'
,'z'
- After receiving all four characters, the stream is
"axyz"
- The suffix
"xyz"
matches one of the words in the array - So
query('z')
would returntrue
The solution uses a Trie (prefix tree) with reversed words approach:
- All words are inserted into the Trie in reverse order (e.g.,
"abc"
becomes"cba"
) - As characters arrive, they're stored in a list
- For each query, the solution searches the Trie by traversing the character list in reverse order (most recent character first)
- If at any point during the traversal a word ending is found in the Trie, it means a matching suffix exists
- The
limit
of 201 characters is used to optimize memory by only keeping the most recent characters (since the maximum word length in typical constraints is 200)
This approach efficiently handles the suffix matching requirement by converting it into a prefix matching problem through reversal.
Intuition
The key insight is recognizing that checking for suffixes in a stream is computationally expensive if done naively. For each new character, we'd need to check all possible suffixes against all words, which would be inefficient.
Let's think about what happens when we check for suffixes. If our stream is "axyz"
, we need to check if "z"
, "yz"
, "xyz"
, or "axyz"
match any word. Notice that we're always checking from the end of the stream backwards.
This observation leads to a clever transformation: what if we reverse the problem? Instead of checking suffixes from the end, we can check prefixes from the beginning by reversing both the words and the search order.
Here's the thought process:
-
If we store words in reverse (e.g.,
"xyz"
becomes"zyx"
), then checking if"xyz"
is a suffix of the stream becomes checking if"zyx"
is a prefix when we read the stream backwards. -
Reading the stream backwards means starting from the most recent character. So for stream
"axyz"
, we'd check in order:"z"
,"zy"
,"zyx"
,"zyxa"
. -
A Trie is perfect for prefix matching! As we traverse character by character, we can immediately know if we're forming a valid prefix of any reversed word.
-
The beauty of this approach is that we can stop early - if at any point during our backwards traversal we find a complete word in the Trie (marked by
is_end = True
), we've found a matching suffix.
The limit
of 201 characters comes from practical constraints - we don't need to keep the entire stream history, just enough to cover the longest possible word (typically 200 characters in LeetCode problems) plus one extra character.
This reversal technique transforms a suffix-matching problem into a prefix-matching problem, allowing us to leverage the Trie's O(m) search complexity where m is the length of the word being checked.
Learn more about Trie and Data Stream patterns.
Solution Approach
The solution implements two classes: a [Trie](/problems/trie_intro)
data structure for efficient prefix matching and the main StreamChecker
class.
Trie Implementation:
The [Trie](/problems/trie_intro)
class has three main components:
children
: An array of 26 elements (for lowercase letters a-z), where each element can point to another Trie nodeis_end
: A boolean flag marking if a word ends at this nodeinsert(w)
: Inserts a word in reverse order into the Triesearch(w)
: Searches for any word that matches when traversing the input in reverse
The insert
method works by:
- Starting from the root node
- Iterating through the word in reverse (
w[::-1]
) - For each character, calculating its index (
ord(c) - ord('a')
) - Creating new Trie nodes as needed along the path
- Marking the final node with
is_end = True
The search
method:
- Traverses the input list in reverse order (
w[::-1]
) - For each character, follows the corresponding child node
- If no child exists, returns
False
(no matching word) - If it finds a node with
is_end = True
, returnsTrue
immediately (found a matching suffix)
StreamChecker Implementation:
The StreamChecker
class maintains:
self.[trie](/problems/trie_intro)
: The Trie containing all reversed wordsself.cs
: A list storing the stream of charactersself.limit
: Set to 201 to optimize memory usage
During initialization:
def __init__(self, words: List[str]):
self.[trie](/problems/trie_intro) = Trie()
self.cs = []
self.limit = 201
for w in words:
self.trie.insert(w)
Each word is inserted into the Trie in reverse order.
The query
method:
def query(self, letter: str) -> bool:
self.cs.append(letter)
return self.[trie](/problems/trie_intro).search(self.cs[-self.limit :])
- Appends the new character to the stream list
- Passes only the last 201 characters to the search method (using slicing
self.cs[-self.limit:]
) - The search automatically checks all possible suffixes by traversing backwards
Time Complexity:
- Initialization: O(n × m) where n is the number of words and m is the average word length
- Query: O(min(m, 201)) where m is the longest word length
Space Complexity:
- O(n × m) for the Trie structure
- O(201) for the character stream buffer
The clever use of reversal transforms suffix matching into prefix matching, making the Trie an ideal data structure for this problem.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with words = ["cd", "f", "kl"]
and a stream of queries.
Step 1: Build the Trie with Reversed Words
- Insert "cd" reversed → "dc"
- Insert "f" reversed → "f"
- Insert "kl" reversed → "lk"
Our Trie structure looks like:
root / | \ d f l | * | c k * * (* indicates is_end = True)
Step 2: Process Queries
Query 1: query('a')
- Stream becomes:
['a']
- Search path in Trie (reading backwards): Start with 'a'
- No child at index 'a' from root → Return
false
Query 2: query('b')
- Stream becomes:
['a', 'b']
- Search path: 'b' → 'ba' (reading backwards)
- No child at index 'b' from root → Return
false
Query 3: query('c')
- Stream becomes:
['a', 'b', 'c']
- Search path: 'c' → 'cb' → 'cba'
- No child at index 'c' from root → Return
false
Query 4: query('d')
- Stream becomes:
['a', 'b', 'c', 'd']
- Search path: 'd' → 'dc' → 'dcb' → 'dcba'
- Follow 'd' from root (exists)
- Follow 'c' from 'd' node (exists, has
is_end = True
) - Found complete word "dc" (which is "cd" reversed) → Return
true
- This means "cd" is a suffix of "abcd"
Query 5: query('e')
- Stream becomes:
['a', 'b', 'c', 'd', 'e']
- Search path: 'e' → 'ed' → 'edc' → 'edcb' → 'edcba'
- No child at index 'e' from root → Return
false
Query 6: query('f')
- Stream becomes:
['a', 'b', 'c', 'd', 'e', 'f']
- Search path: 'f' → 'fe' → 'fed' → 'fedc' → 'fedcb' → 'fedcba'
- Follow 'f' from root (exists, has
is_end = True
) - Found complete word "f" → Return
true
- This means "f" is a suffix of "abcdef"
The key insight: When we traverse the stream backwards and find a complete reversed word in the Trie, it confirms that the original (non-reversed) word is a suffix of our current stream.
Solution Implementation
1from typing import List
2
3
4class Trie:
5 """A trie data structure that stores words in reverse order for suffix matching."""
6
7 def __init__(self):
8 # Array to store 26 possible children (one for each lowercase letter)
9 self.children = [None] * 26
10 # Flag to mark if this node represents the end of a word
11 self.is_end = False
12
13 def insert(self, word: str) -> None:
14 """
15 Insert a word into the trie in reverse order.
16
17 Args:
18 word: The word to insert into the trie
19 """
20 current_node = self
21 # Traverse the word in reverse order
22 for char in word[::-1]:
23 # Calculate the index for this character (0-25)
24 index = ord(char) - ord('a')
25 # Create a new node if it doesn't exist
26 if current_node.children[index] is None:
27 current_node.children[index] = Trie()
28 # Move to the child node
29 current_node = current_node.children[index]
30 # Mark the end of the word
31 current_node.is_end = True
32
33 def search(self, characters: List[str]) -> bool:
34 """
35 Search if any suffix of the character list matches a word in the trie.
36
37 Args:
38 characters: List of characters to search
39
40 Returns:
41 True if any suffix matches a word in the trie, False otherwise
42 """
43 current_node = self
44 # Traverse the characters in reverse order (checking suffixes)
45 for char in characters[::-1]:
46 # Calculate the index for this character
47 index = ord(char) - ord('a')
48 # If no matching path exists, return False
49 if current_node.children[index] is None:
50 return False
51 # Move to the child node
52 current_node = current_node.children[index]
53 # If we've found a complete word, return True
54 if current_node.is_end:
55 return True
56 return False
57
58
59class StreamChecker:
60 """
61 A class to check if any suffix of a character stream matches words from a dictionary.
62 """
63
64 def __init__(self, words: List[str]):
65 """
66 Initialize the StreamChecker with a list of words.
67
68 Args:
69 words: List of words to match against the stream
70 """
71 # Initialize the trie for storing words
72 self.trie = Trie()
73 # List to store the stream of characters
74 self.character_stream = []
75 # Maximum number of recent characters to keep (optimization)
76 self.max_length = 201
77
78 # Insert all words into the trie
79 for word in words:
80 self.trie.insert(word)
81
82 def query(self, letter: str) -> bool:
83 """
84 Add a letter to the stream and check if any suffix matches a word.
85
86 Args:
87 letter: The new character to add to the stream
88
89 Returns:
90 True if any suffix of the stream matches a word, False otherwise
91 """
92 # Add the new letter to the stream
93 self.character_stream.append(letter)
94 # Search only the last max_length characters (optimization for memory)
95 return self.trie.search(self.character_stream[-self.max_length:])
96
97
98# Your StreamChecker object will be instantiated and called as such:
99# obj = StreamChecker(words)
100# param_1 = obj.query(letter)
101
1/**
2 * Trie (Prefix Tree) implementation for reverse string matching.
3 * This Trie stores strings in reverse order to efficiently check suffixes.
4 */
5class Trie {
6 // Array to store 26 child nodes (one for each lowercase letter)
7 Trie[] children = new Trie[26];
8 // Flag to mark if current node represents end of a word
9 boolean isEnd = false;
10
11 /**
12 * Inserts a word into the Trie in reverse order.
13 * @param word The word to be inserted
14 */
15 public void insert(String word) {
16 Trie currentNode = this;
17 // Traverse the word from right to left (reverse order)
18 for (int i = word.length() - 1; i >= 0; i--) {
19 int charIndex = word.charAt(i) - 'a';
20 // Create new node if path doesn't exist
21 if (currentNode.children[charIndex] == null) {
22 currentNode.children[charIndex] = new Trie();
23 }
24 // Move to the child node
25 currentNode = currentNode.children[charIndex];
26 }
27 // Mark the end of the word
28 currentNode.isEnd = true;
29 }
30
31 /**
32 * Queries if any word in the Trie matches a suffix of the given string.
33 * @param stringBuilder The StringBuilder containing the stream of characters
34 * @return true if any word matches a suffix, false otherwise
35 */
36 public boolean query(StringBuilder stringBuilder) {
37 Trie currentNode = this;
38 // Traverse the string from right to left (checking suffixes)
39 for (int i = stringBuilder.length() - 1; i >= 0; i--) {
40 int charIndex = stringBuilder.charAt(i) - 'a';
41 // If path doesn't exist, no word matches
42 if (currentNode.children[charIndex] == null) {
43 return false;
44 }
45 // Move to the child node
46 currentNode = currentNode.children[charIndex];
47 // If we found a complete word, return true
48 if (currentNode.isEnd) {
49 return true;
50 }
51 }
52 return false;
53 }
54}
55
56/**
57 * StreamChecker class to check if any suffix of the stream matches a word from dictionary.
58 * Uses a Trie data structure with reverse insertion for efficient suffix matching.
59 */
60class StreamChecker {
61 // StringBuilder to maintain the stream of characters
62 private StringBuilder streamBuilder = new StringBuilder();
63 // Trie to store dictionary words in reverse order
64 private Trie trie = new Trie();
65
66 /**
67 * Constructor initializes the StreamChecker with a dictionary of words.
68 * @param words Array of words to be stored in the Trie
69 */
70 public StreamChecker(String[] words) {
71 // Insert all words into the Trie
72 for (String word : words) {
73 trie.insert(word);
74 }
75 }
76
77 /**
78 * Adds a letter to the stream and checks if any suffix matches a dictionary word.
79 * @param letter The character to be added to the stream
80 * @return true if any suffix of the stream matches a word, false otherwise
81 */
82 public boolean query(char letter) {
83 // Append the new letter to the stream
84 streamBuilder.append(letter);
85 // Check if any suffix matches a word in the Trie
86 return trie.query(streamBuilder);
87 }
88}
89
90/**
91 * Your StreamChecker object will be instantiated and called as such:
92 * StreamChecker obj = new StreamChecker(words);
93 * boolean param_1 = obj.query(letter);
94 */
95
1class Trie {
2private:
3 // Array to store pointers to child nodes (26 letters)
4 Trie* children[26]{};
5 // Flag to mark if this node represents the end of a word
6 bool isEndOfWord = false;
7
8public:
9 // Insert a word into the Trie (stored in reverse order)
10 void insert(string& word) {
11 Trie* currentNode = this;
12
13 // Reverse the word to enable suffix matching from the end
14 reverse(word.begin(), word.end());
15
16 // Insert each character of the reversed word
17 for (char& ch : word) {
18 int charIndex = ch - 'a';
19
20 // Create a new node if it doesn't exist
21 if (!currentNode->children[charIndex]) {
22 currentNode->children[charIndex] = new Trie();
23 }
24
25 currentNode = currentNode->children[charIndex];
26 }
27
28 // Mark the end of the word
29 currentNode->isEndOfWord = true;
30 }
31
32 // Search for any word ending at the current position in the stream
33 bool search(string& stream) {
34 Trie* currentNode = this;
35
36 // Search from the end of the stream backwards
37 // Limit search to 201 characters (constraint from problem)
38 for (int streamIdx = stream.size() - 1, searchLength = 0;
39 streamIdx >= 0 && searchLength < 201;
40 --streamIdx, ++searchLength) {
41
42 int charIndex = stream[streamIdx] - 'a';
43
44 // If character doesn't exist in Trie, no match possible
45 if (!currentNode->children[charIndex]) {
46 return false;
47 }
48
49 currentNode = currentNode->children[charIndex];
50
51 // If we found a complete word, return true
52 if (currentNode->isEndOfWord) {
53 return true;
54 }
55 }
56
57 return false;
58 }
59};
60
61class StreamChecker {
62private:
63 // Trie to store all words in reverse order
64 Trie* trie = new Trie();
65 // Accumulated stream of characters
66 string stream;
67
68public:
69 // Constructor: Initialize the Trie with given words
70 StreamChecker(vector<string>& words) {
71 for (auto& word : words) {
72 trie->insert(word);
73 }
74 }
75
76 // Query: Add a letter to the stream and check if any suffix matches a word
77 bool query(char letter) {
78 stream += letter;
79 return trie->search(stream);
80 }
81};
82
83/**
84 * Your StreamChecker object will be instantiated and called as such:
85 * StreamChecker* obj = new StreamChecker(words);
86 * bool param_1 = obj->query(letter);
87 */
88
1// Node structure for Trie
2interface TrieNode {
3 // Map to store child nodes (26 letters, using character as key)
4 children: Map<string, TrieNode>;
5 // Flag to mark if this node represents the end of a word
6 isEndOfWord: boolean;
7}
8
9// Global variables for StreamChecker
10let trieRoot: TrieNode;
11let stream: string;
12
13// Helper function to create a new Trie node
14function createTrieNode(): TrieNode {
15 return {
16 children: new Map<string, TrieNode>(),
17 isEndOfWord: false
18 };
19}
20
21// Insert a word into the Trie (stored in reverse order)
22function insertWord(word: string): void {
23 let currentNode = trieRoot;
24
25 // Reverse the word to enable suffix matching from the end
26 const reversedWord = word.split('').reverse().join('');
27
28 // Insert each character of the reversed word
29 for (const char of reversedWord) {
30 // Create a new node if it doesn't exist
31 if (!currentNode.children.has(char)) {
32 currentNode.children.set(char, createTrieNode());
33 }
34
35 currentNode = currentNode.children.get(char)!;
36 }
37
38 // Mark the end of the word
39 currentNode.isEndOfWord = true;
40}
41
42// Search for any word ending at the current position in the stream
43function searchInStream(): boolean {
44 let currentNode = trieRoot;
45
46 // Search from the end of the stream backwards
47 // Limit search to 201 characters (constraint from problem)
48 let searchLength = 0;
49 for (let streamIndex = stream.length - 1;
50 streamIndex >= 0 && searchLength < 201;
51 streamIndex--, searchLength++) {
52
53 const char = stream[streamIndex];
54
55 // If character doesn't exist in Trie, no match possible
56 if (!currentNode.children.has(char)) {
57 return false;
58 }
59
60 currentNode = currentNode.children.get(char)!;
61
62 // If we found a complete word, return true
63 if (currentNode.isEndOfWord) {
64 return true;
65 }
66 }
67
68 return false;
69}
70
71// Constructor: Initialize the Trie with given words
72function StreamChecker(words: string[]): void {
73 // Initialize the trie root
74 trieRoot = createTrieNode();
75 // Initialize the stream as empty
76 stream = '';
77
78 // Insert all words into the trie
79 for (const word of words) {
80 insertWord(word);
81 }
82}
83
84// Query: Add a letter to the stream and check if any suffix matches a word
85function query(letter: string): boolean {
86 // Append the letter to the stream
87 stream += letter;
88 // Check if any suffix of the stream matches a word in the trie
89 return searchInStream();
90}
91
Time and Space Complexity
Time Complexity:
-
Initialization (
__init__
):O(M * L)
whereM
is the number of words andL
is the average length of each word. Each word insertion into the Trie takesO(L)
time, and we insertM
words. -
Query (
query
):O(min(L_max, limit))
whereL_max
is the length of the longest word in the dictionary andlimit = 201
. The search operation traverses at mostmin(201, L_max)
characters in reverse order through the Trie. Since we're only searching the last 201 characters (self.cs[-self.limit:]
), the search is bounded by this constant.
Space Complexity:
-
Trie Structure:
O(M * L * 26)
in the worst case, whereM
is the number of words andL
is the average length of words. Each Trie node can have up to 26 children (one for each lowercase letter). In practice, the space usage is typically much less due to shared prefixes (or in this case, shared suffixes since words are inserted in reverse). -
Character Stream Storage (
self.cs
):O(Q)
whereQ
is the number of queries made. The list grows with each query call and stores all queried characters. -
Overall Space Complexity:
O(M * L * 26 + Q)
, dominated by the Trie structure and the accumulated character stream.
Note: The code stores words in reverse order in the Trie and searches from the most recent characters backwards, which allows efficient checking if any word ends at the current position in the stream.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Reverse Words During Insertion
One of the most common mistakes is inserting words into the Trie in their original order instead of reversed order. This breaks the entire suffix-matching logic.
Incorrect Implementation:
def insert(self, word: str) -> None:
current_node = self
# WRONG: Iterating in normal order
for char in word:
index = ord(char) - ord('a')
if current_node.children[index] is None:
current_node.children[index] = Trie()
current_node = current_node.children[index]
current_node.is_end = True
Why This Fails:
- If we insert "abc" normally, the Trie stores: a → b → c
- When searching for suffix "bc" in stream "abc", we traverse "cb" (reversed)
- The Trie won't find "cb" because it has "abc", not "cba"
Correct Implementation:
def insert(self, word: str) -> None:
current_node = self
# CORRECT: Reverse the word first
for char in word[::-1]:
index = ord(char) - ord('a')
if current_node.children[index] is None:
current_node.children[index] = Trie()
current_node = current_node.children[index]
current_node.is_end = True
2. Not Limiting the Character Stream Buffer
Another pitfall is storing the entire stream without any limit, which can cause memory issues for long-running streams.
Problematic Implementation:
def query(self, letter: str) -> bool:
self.character_stream.append(letter)
# PROBLEM: Searching entire stream history
return self.trie.search(self.character_stream)
Why This Is Problematic:
- Memory usage grows unbounded with each query
- Search time increases unnecessarily for very long streams
- Since words have a maximum length (typically 200), checking beyond that is wasteful
Optimized Solution:
def query(self, letter: str) -> bool:
self.character_stream.append(letter)
# Only search the last 201 characters
return self.trie.search(self.character_stream[-self.max_length:])
3. Incorrect Search Termination Logic
A subtle bug occurs when the search method doesn't properly check for word endings during traversal.
Incorrect Implementation:
def search(self, characters: List[str]) -> bool:
current_node = self
for char in characters[::-1]:
index = ord(char) - ord('a')
if current_node.children[index] is None:
return False
current_node = current_node.children[index]
# WRONG: Only checking at the end of traversal
return current_node.is_end
Why This Fails:
- This only checks if the entire reversed stream matches a word
- It misses shorter suffixes that might match
- For stream "axyz" with word "xyz", it would check if "zyxa" is a word, missing that "zyx" matches "xyz"
Correct Implementation:
def search(self, characters: List[str]) -> bool:
current_node = self
for char in characters[::-1]:
index = ord(char) - ord('a')
if current_node.children[index] is None:
return False
current_node = current_node.children[index]
# CORRECT: Check for word ending at each step
if current_node.is_end:
return True
return False
4. Character Index Calculation Error
Using incorrect ASCII value calculations can lead to array index out of bounds errors.
Common Mistake:
# WRONG: Using 'A' for lowercase letters
index = ord(char) - ord('A')
# WRONG: Not handling the index calculation at all
index = ord(char)
Correct Approach:
# For lowercase letters only
index = ord(char) - ord('a') # Gives 0-25 for 'a'-'z'
These pitfalls highlight the importance of understanding the reversal strategy and careful implementation of both the Trie operations and the stream buffer management.
Which of the following is a good use case for backtracking?
Recommended Readings
Trie Introduction Imagine you have a magic dictionary where as soon as you start spelling a word it immediately tells you how many words that start with those letters How cool would that be This is exactly the kind of magic a data structure called trie performs What's a Trie
Median of Data Stream Given a stream of numbers find the median number at any given time accurate to 1 decimal place Example add_number 1 add_number 2 add_number 3 get_median 2 0 add_number 4 get_median 2 5 Try it yourself Explanation Intuition Brute force way is to sort the entire list every time we get a new number This would be O N log N for each add_number operation One step up is to notice that the list is sorted before we add any new number to it So every
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
Want a Structured Path to Master System Design Too? Don’t Miss This!