Facebook Pixel

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:

  1. Constructor StreamChecker(String[] words): Initializes the stream checker with an array of words that you'll be checking against.

  2. Method query(char letter): Takes a single character as input, adds it to the stream, and returns true if any suffix of the current stream matches any word from the initial word list, otherwise returns false.

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 return true

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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.

  2. Reading the stream backwards means starting from the most recent character. So for stream "axyz", we'd check in order: "z", "zy", "zyx", "zyxa".

  3. 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.

  4. 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 node
  • is_end: A boolean flag marking if a word ends at this node
  • insert(w): Inserts a word in reverse order into the Trie
  • search(w): Searches for any word that matches when traversing the input in reverse

The insert method works by:

  1. Starting from the root node
  2. Iterating through the word in reverse (w[::-1])
  3. For each character, calculating its index (ord(c) - ord('a'))
  4. Creating new Trie nodes as needed along the path
  5. Marking the final node with is_end = True

The search method:

  1. Traverses the input list in reverse order (w[::-1])
  2. For each character, follows the corresponding child node
  3. If no child exists, returns False (no matching word)
  4. If it finds a node with is_end = True, returns True immediately (found a matching suffix)

StreamChecker Implementation:

The StreamChecker class maintains:

  • self.[trie](/problems/trie_intro): The Trie containing all reversed words
  • self.cs: A list storing the stream of characters
  • self.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 :])
  1. Appends the new character to the stream list
  2. Passes only the last 201 characters to the search method (using slicing self.cs[-self.limit:])
  3. 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 Evaluator

Example 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) where M is the number of words and L is the average length of each word. Each word insertion into the Trie takes O(L) time, and we insert M words.

  • Query (query): O(min(L_max, limit)) where L_max is the length of the longest word in the dictionary and limit = 201. The search operation traverses at most min(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, where M is the number of words and L 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) where Q 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More