Stream of Characters
Streaming multi-pattern match — build an Aho-Corasick-like trie over reversed words, walk it with a buffer of recent chars.
5 min read
design trie streaming
Problem#
StreamChecker is initialized with a list of words. query(letter) returns true iff some word ends at the current stream position (i.e., is a suffix of all chars seen so far).
Examples#
words=["cd","f","kl"]; query('a')→false;query('b')→false;query('c')→false;query('d')→true(“cd”).
Constraints#
1 <= words.length <= 2000, total chars<= 2 * 10^4, up to4 * 10^4queries.
Approach#
Insert each word reversed into a trie. Maintain a growing buffer of all chars seen. On each query, walk backward through recent chars (up to the longest word) along the trie; if we ever reach an end-of-word marker, return true.
Solution#
class StreamChecker { struct Node { Node* ch[26] = {}; bool end = false; }; Node* root = new Node(); string buf; int maxLen = 0;public: StreamChecker(vector<string>& words) { for (auto& w : words) { maxLen = max(maxLen, (int)w.size()); Node* cur = root; for (int i = (int)w.size() - 1; i >= 0; --i) { int c = w[i] - 'a'; if (!cur->ch[c]) cur->ch[c] = new Node(); cur = cur->ch[c]; } cur->end = true; } } bool query(char letter) { buf.push_back(letter); Node* cur = root; int end = (int)buf.size() - 1, lo = max(0, end - maxLen + 1); for (int i = end; i >= lo; --i) { int c = buf[i] - 'a'; if (!cur->ch[c]) return false; cur = cur->ch[c]; if (cur->end) return true; } return false; }};type scNode struct { ch [26]*scNode end bool}
type StreamChecker struct { root *scNode buf []byte maxLen int}
func Constructor(words []string) StreamChecker { sc := StreamChecker{root: &scNode{}} for _, w := range words { if len(w) > sc.maxLen { sc.maxLen = len(w) } cur := sc.root for i := len(w) - 1; i >= 0; i-- { c := w[i] - 'a' if cur.ch[c] == nil { cur.ch[c] = &scNode{} } cur = cur.ch[c] } cur.end = true } return sc}
func (sc *StreamChecker) Query(letter byte) bool { sc.buf = append(sc.buf, letter) cur := sc.root end := len(sc.buf) - 1 lo := end - sc.maxLen + 1 if lo < 0 { lo = 0 } for i := end; i >= lo; i-- { c := sc.buf[i] - 'a' if cur.ch[c] == nil { return false } cur = cur.ch[c] if cur.end { return true } } return false}from typing import List
class _Node: __slots__ = ("ch", "end") def __init__(self) -> None: self.ch: dict[str, "_Node"] = {} self.end: bool = False
class StreamChecker: def __init__(self, words: List[str]): self.root = _Node() self.buf: List[str] = [] self.max_len = 0 for w in words: self.max_len = max(self.max_len, len(w)) cur = self.root for c in reversed(w): if c not in cur.ch: cur.ch[c] = _Node() cur = cur.ch[c] cur.end = True
def query(self, letter: str) -> bool: self.buf.append(letter) cur = self.root end = len(self.buf) - 1 lo = max(0, end - self.max_len + 1) for i in range(end, lo - 1, -1): c = self.buf[i] if c not in cur.ch: return False cur = cur.ch[c] if cur.end: return True return Falseclass StreamChecker { constructor(words) { this.root = { ch: new Map(), end: false }; this.buf = []; this.maxLen = 0; for (const w of words) { if (w.length > this.maxLen) this.maxLen = w.length; let cur = this.root; for (let i = w.length - 1; i >= 0; i--) { const c = w[i]; if (!cur.ch.has(c)) cur.ch.set(c, { ch: new Map(), end: false }); cur = cur.ch.get(c); } cur.end = true; } }
query(letter) { this.buf.push(letter); let cur = this.root; const end = this.buf.length - 1; const lo = Math.max(0, end - this.maxLen + 1); for (let i = end; i >= lo; i--) { const c = this.buf[i]; if (!cur.ch.has(c)) return false; cur = cur.ch.get(c); if (cur.end) return true; } return false; }}class Node { Node[] ch = new Node[26]; boolean end = false;}
class StreamChecker { private Node root = new Node(); private StringBuilder buf = new StringBuilder(); private int maxLen = 0;
public StreamChecker(String[] words) { for (String w : words) { if (w.length() > maxLen) maxLen = w.length(); Node cur = root; for (int i = w.length() - 1; i >= 0; i--) { int c = w.charAt(i) - 'a'; if (cur.ch[c] == null) cur.ch[c] = new Node(); cur = cur.ch[c]; } cur.end = true; } }
public boolean query(char letter) { buf.append(letter); Node cur = root; int end = buf.length() - 1; int lo = Math.max(0, end - maxLen + 1); for (int i = end; i >= lo; i--) { int c = buf.charAt(i) - 'a'; if (cur.ch[c] == null) return false; cur = cur.ch[c]; if (cur.end) return true; } return false; }}interface TrieNode { ch: Map<string, TrieNode>; end: boolean;}
class StreamChecker { private root: TrieNode; private buf: string[]; private maxLen: number;
constructor(words: string[]) { this.root = { ch: new Map(), end: false }; this.buf = []; this.maxLen = 0; for (const w of words) { if (w.length > this.maxLen) this.maxLen = w.length; let cur = this.root; for (let i = w.length - 1; i >= 0; i--) { const c = w[i]; if (!cur.ch.has(c)) cur.ch.set(c, { ch: new Map(), end: false }); cur = cur.ch.get(c)!; } cur.end = true; } }
query(letter: string): boolean { this.buf.push(letter); let cur = this.root; const end = this.buf.length - 1; const lo = Math.max(0, end - this.maxLen + 1); for (let i = end; i >= lo; i--) { const c = this.buf[i]; if (!cur.ch.has(c)) return false; cur = cur.ch.get(c)!; if (cur.end) return true; } return false; }}Editorial#
Reversing the words lets us start matching from the newest character — exactly the data we have. Walking back at most maxLen chars bounds each query’s work, and the trie sharing keeps memory tight.
Complexity#
- Time:
O(maxLen)per query. - Space:
O(sum of word lengths).
Concept revision#
Related#