The "Stream of Characters" problem asks you to implement a data structure that can efficiently determine, after each character input, whether the current sequence of the most recent characters forms any word from a given list of words.
words
at initialization.query(letter)
function.query
, return true
if the sequence of the most recent characters (ending with the newest letter) forms any word in words
as a suffix; otherwise, return false
.words
can be used multiple times, and the stream can be very long.Constraints:
words.length
≤ 2000words[i].length
≤ 200At first glance, the problem seems to require checking, after each new letter, whether any suffix of the current stream matches any of the words in the list. A brute-force approach would be to, on each query, compare all possible suffixes of the current stream to all words. But with up to 40,000 queries and 2,000 words (each up to 200 characters), this would be far too slow.
We need a way to efficiently check whether the most recent characters form any word in the list, ideally in constant or near-constant time per query. This suggests using a data structure that can quickly check for suffix matches as the stream grows.
The challenge is that, unlike prefix matching (which is well-served by a standard Trie), we need to check for suffixes. This leads to the idea of reversing all words and building a Trie of the reversed words, so the problem becomes prefix-matching on the reversed stream.
Here’s how we can solve the problem efficiently:
words
, reverse it. This lets us check for suffixes by matching prefixes on the reversed stream.
true
; otherwise, return false
.
This approach ensures that each query is handled in time proportional to the maximum word length, not the total number of words or stream length.
Suppose words = ["cd", "f", "kl"]
. We reverse and build a Trie for ["dc", "f", "lk"].
false
.
false
.
false
.
true
.
true
.
false
.
false
.
true
.
In this problem, we efficiently check for word suffixes in a character stream by reversing all words and building a Trie for quick prefix matching on the reversed stream. This allows us to answer each query in time proportional to the longest word, making the solution both elegant and performant. The approach highlights the power of Tries for string matching problems, especially when combined with clever transformations like reversing for suffix checks.
class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
class StreamChecker:
def __init__(self, words):
self.root = TrieNode()
self.max_len = 0
for word in words:
self.max_len = max(self.max_len, len(word))
node = self.root
for c in reversed(word):
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.is_word = True
self.stream = []
def query(self, letter):
self.stream.append(letter)
if len(self.stream) > self.max_len:
self.stream.pop(0)
node = self.root
for c in reversed(self.stream):
if c not in node.children:
return False
node = node.children[c]
if node.is_word:
return True
return False
class TrieNode {
public:
TrieNode* children[26];
bool is_word;
TrieNode() : is_word(false) {
memset(children, 0, sizeof(children));
}
};
class StreamChecker {
TrieNode* root;
int max_len;
std::deque<char> stream;
public:
StreamChecker(vector<string>& words) {
root = new TrieNode();
max_len = 0;
for (const auto& word : words) {
max_len = max(max_len, (int)word.size());
TrieNode* node = root;
for (int i = word.size() - 1; i >= 0; --i) {
int idx = word[i] - 'a';
if (!node->children[idx])
node->children[idx] = new TrieNode();
node = node->children[idx];
}
node->is_word = true;
}
}
bool query(char letter) {
stream.push_back(letter);
if (stream.size() > max_len)
stream.pop_front();
TrieNode* node = root;
for (auto it = stream.rbegin(); it != stream.rend(); ++it) {
int idx = *it - 'a';
if (!node->children[idx])
return false;
node = node->children[idx];
if (node->is_word)
return true;
}
return false;
}
};
class TrieNode {
TrieNode[] children = new TrieNode[26];
boolean isWord = false;
}
class StreamChecker {
TrieNode root = new TrieNode();
int maxLen = 0;
Deque<Character> stream = new ArrayDeque<>();
public StreamChecker(String[] words) {
for (String word : words) {
maxLen = Math.max(maxLen, word.length());
TrieNode node = root;
for (int i = word.length() - 1; i >= 0; --i) {
int idx = word.charAt(i) - 'a';
if (node.children[idx] == null)
node.children[idx] = new TrieNode();
node = node.children[idx];
}
node.isWord = true;
}
}
public boolean query(char letter) {
stream.addLast(letter);
if (stream.size() > maxLen)
stream.removeFirst();
TrieNode node = root;
Iterator<Character> it = stream.descendingIterator();
while (it.hasNext()) {
char c = it.next();
int idx = c - 'a';
if (node.children[idx] == null)
return false;
node = node.children[idx];
if (node.isWord)
return true;
}
return false;
}
}
class TrieNode {
constructor() {
this.children = {};
this.isWord = false;
}
}
class StreamChecker {
constructor(words) {
this.root = new TrieNode();
this.maxLen = 0;
for (let word of words) {
this.maxLen = Math.max(this.maxLen, word.length);
let node = this.root;
for (let i = word.length - 1; i >= 0; --i) {
let c = word[i];
if (!node.children[c])
node.children[c] = new TrieNode();
node = node.children[c];
}
node.isWord = true;
}
this.stream = [];
}
query(letter) {
this.stream.push(letter);
if (this.stream.length > this.maxLen)
this.stream.shift();
let node = this.root;
for (let i = this.stream.length - 1; i >= 0; --i) {
let c = this.stream[i];
if (!node.children[c])
return false;
node = node.children[c];
if (node.isWord)
return true;
}
return false;
}
}