You must design a data structure that supports:
StreamChecker(String[] words)
query(char letter) → returns true if any word in words is a suffix
of the streamed characters so far
Meaning:
- Characters arrive one by one
- After each query, you check whether the sequence of characters seen so far ends with any word from the given list
- Return
trueif at least one word matches as a suffix, elsefalse
Example:
Input:
words = ["cd","f","kl"]
Queries and output:
query('a') → false
query('b') → false
query('c') → false
query('d') → true (stream ends with "cd")
query('k') → false
query('l') → true (stream ends with "kl")
Problem Understanding
Key observations:
- You do not search for substrings anywhere — only suffix matches
- Stream keeps growing, but you only care about latest characters
- Matching must be efficient because:
- up to 2000 words
- each up to length 2000
- up to 40,000 queries
Brute force checking becomes far too slow.
So we need a smarter way to detect whether the ending of the current stream matches any word.
Logic Explained in Simple English
The fastest method relies on a key insight:
Instead of storing words normally, store them reversed in a Trie.
Why?
- The query asks whether the end of the stream matches any word
- If we reverse all words, then matching suffixes becomes matching prefixes
- When new characters arrive, we check from newest backward
So:
- Reverse each word and insert into a Trie
- Maintain a running character stream (as a StringBuilder or list)
- On each query:
- append new character
- walk the Trie using stream characters in reverse order
- if we reach a terminal Trie node → return true
- if we hit a null child → return false
Also important:
We do not need to store the entire stream forever.
Why?
- Only the last maxWordLength characters can ever matter
- Anything earlier cannot form a suffix match
So after appending, we can trim length if needed.
Step-by-Step Approach
Step 1: Build reversed Trie
- Each node has up to 26 children (lowercase letters)
- A boolean flag marks the end of a valid word
Step 2: Track stream characters
- Use a StringBuilder or deque
- Append each new query character to the end
- If stream length exceeds max word length, drop earliest characters
Step 3: Query processing
- Add character to stream
- Starting from end of stream, walk down Trie:
- If child missing → return false
- If we hit an end-of-word flag → return true
- If traversal ends without match → return false
Java Implementation
class StreamChecker {
private static class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isWord;
}
private TrieNode root;
private StringBuilder stream;
private int maxLen;
public StreamChecker(String[] words) {
root = new TrieNode();
stream = new StringBuilder();
maxLen = 0;
for (String w : words) {
maxLen = Math.max(maxLen, w.length());
insertReversed(w);
}
}
private void insertReversed(String word) {
TrieNode curr = root;
for (int i = word.length() - 1; i >= 0; i--) {
int idx = word.charAt(i) - 'a';
if (curr.children[idx] == null) {
curr.children[idx] = new TrieNode();
}
curr = curr.children[idx];
}
curr.isWord = true;
}
public boolean query(char letter) {
stream.append(letter);
if (stream.length() > maxLen) {
stream.deleteCharAt(0);
}
TrieNode curr = root;
for (int i = stream.length() - 1; i >= 0; i--) {
int idx = stream.charAt(i) - 'a';
if (curr.children[idx] == null) {
return false;
}
curr = curr.children[idx];
if (curr.isWord) {
return true;
}
}
return false;
}
}
Dry Run Example
Words:
["cd","f","kl"]
Reversed insertion:
"dc" "f" "lk"
maxLen = 2
Queries:
Stream starts empty
- query(‘a’):
stream = “a”
check reverse: ‘a’ → no match
return false - query(‘b’):
stream = “ab”
check ‘b’ → no child
return false - query(‘c’):
stream = “bc”
check ‘c’ → child exists (part of “cd”)
next char: ‘b’ → no continuation
return false - query(‘d’):
stream = “cd” (maxLen)
check ‘d’ → child exists
next char: ‘c’ → completes “cd” word
return true - query(‘k’):
stream = “dk”
check ‘k’ → child exists (part of “kl”)
next char: ‘d’ → no continuation
return false - query(‘l’):
stream = “kl”
check ‘l’ → child exists
next: ‘k’ → completes “kl”
return true
Time and Space Complexity
Time Complexity
For each query:
O(min(streamLength, maxLen)) → worst O(maxLen)
Since maxLen ≤ 2000, queries are efficient.
Space Complexity
O(sum of word lengths)
Trie stores reversed words
Common Mistakes and How to Avoid Them
Mistake 1: Checking entire stream every time
Only last max-length matters
Mistake 2: Using normal Trie
Suffix matching becomes inefficient — must reverse
Mistake 3: Scanning words instead of Trie traversal
Too slow for repeated queries
Mistake 4: Building full stream indefinitely
Memory grows unnecessarily
Mistake 5: Returning early on first mismatch
Must detect isWord inside traversal
