Trie and String Algorithm Interview Patterns

Tries (prefix trees) and string algorithms appear in autocomplete, spell checking, IP routing, and competitive programming. Mastering trie construction and string pattern matching unlocks a class of problems that hash tables can’t solve efficiently.

Trie Fundamentals

class TrieNode:
    def __init__(self):
        self.children = {}  # char → TrieNode
        self.is_end = False
        self.count = 0      # words passing through this node

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
            node.count += 1
        node.is_end = True

    def search(self, word: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end

    def starts_with(self, prefix: str) -> bool:
        return self._find(prefix) is not None

    def _find(self, prefix: str) -> TrieNode:
        node = self.root
        for ch in prefix:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

Complexity: Insert/Search O(L) time and space, where L = word length. Total space O(ALPHABET_SIZE × N × L).

Pattern 1: Autocomplete / Top K Suggestions

class AutocompleteSystem:
    """LC 642: Design Search Autocomplete System"""
    def __init__(self, sentences: list[str], times: list[int]):
        self.trie = Trie()
        self.freq = defaultdict(int)
        for s, t in zip(sentences, times):
            self.freq[s] = t
            self.trie.insert(s)
        self.cur = []  # current input buffer

    def input(self, c: str) -> list[str]:
        if c == '#':
            word = ''.join(self.cur)
            self.freq[word] += 1
            self.trie.insert(word)
            self.cur = []
            return []
        self.cur.append(c)
        prefix = ''.join(self.cur)
        # DFS from prefix node to collect all completions
        node = self.trie._find(prefix)
        if not node:
            return []
        results = []
        def dfs(n, path):
            if n.is_end:
                results.append((self.freq[path], path))
            for ch, child in sorted(n.children.items()):
                dfs(child, path + ch)
        dfs(node, prefix)
        results.sort(key=lambda x: (-x[0], x[1]))
        return [r[1] for r in results[:3]]

Pattern 2: Word Search II (Trie + DFS)

def findWords(board: list[list[str]], words: list[str]) -> list[str]:
    """LC 212: Find all words on board"""
    trie = Trie()
    for w in words:
        trie.insert(w)

    rows, cols = len(board), len(board[0])
    result = set()

    def dfs(r, c, node, path):
        ch = board[r][c]
        if ch not in node.children:
            return
        node = node.children[ch]
        path += ch
        if node.is_end:
            result.add(path)
            node.is_end = False  # avoid duplicates
        board[r][c] = '#'  # mark visited
        for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:
            nr, nc = r+dr, c+dc
            if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != '#':
                dfs(nr, nc, node, path)
        board[r][c] = ch  # restore

    for r in range(rows):
        for c in range(cols):
            dfs(r, c, trie.root, '')
    return list(result)

# Key optimization: prune trie nodes with no remaining words
# (remove leaf nodes after finding a word to avoid redundant DFS)

Pattern 3: Replace Words (Trie Prefix Matching)

def replaceWords(dictionary: list[str], sentence: str) -> str:
    """LC 648: Replace words with shortest root"""
    trie = Trie()
    for root in dictionary:
        trie.insert(root)

    def replace(word):
        node = trie.root
        for i, ch in enumerate(word):
            if ch not in node.children:
                break
            node = node.children[ch]
            if node.is_end:
                return word[:i+1]  # return shortest prefix
        return word

    return ' '.join(replace(w) for w in sentence.split())

Pattern 4: Palindrome Pairs

def palindromePairs(words: list[str]) -> list[list[int]]:
    """LC 336: Find all (i,j) where words[i]+words[j] is palindrome
    Key insight: insert reversed words into trie; for each word,
    check if any reversed word is a prefix/suffix that creates palindrome"""
    word_map = {w: i for i, w in enumerate(words)}
    result = []

    def is_palindrome(s, l, r):
        while l < r:
            if s[l] != s[r]: return False
            l += 1; r -= 1
        return True

    for i, word in enumerate(words):
        n = len(word)
        # Case 1: word is prefix of reversed_other
        # words[i] + words[j] where words[j] reversed starts with words[i]
        for j in range(n + 1):
            # Check if word[j:] is palindrome; if word[:j] reversed is in map
            if is_palindrome(word, j, n-1):
                rev = word[:j][::-1]
                if rev in word_map and word_map[rev] != i:
                    result.append([i, word_map[rev]])
            # Check if word[:j] is palindrome; if word[j:] reversed is in map
            if j != n and is_palindrome(word, 0, j-1):
                rev = word[j:][::-1]
                if rev in word_map and word_map[rev] != i:
                    result.append([word_map[rev], i])
    return result

Pattern 5: Implement Magic Dictionary (Wildcard)

class MagicDictionary:
    """LC 676: Search with exactly one character changed"""
    def buildDict(self, dictionary: list[str]) -> None:
        self.words = set(dictionary)

    def search(self, searchWord: str) -> bool:
        for i in range(len(searchWord)):
            for c in 'abcdefghijklmnopqrstuvwxyz':
                if c != searchWord[i]:
                    candidate = searchWord[:i] + c + searchWord[i+1:]
                    if candidate in self.words:
                        return True
        return False

# Trie variant with wildcard matching:
def search_with_dot(node, word, idx):
    """Search where '.' matches any character (LC 211)"""
    if idx == len(word):
        return node.is_end
    ch = word[idx]
    if ch == '.':
        return any(search_with_dot(child, word, idx+1)
                   for child in node.children.values())
    if ch not in node.children:
        return False
    return search_with_dot(node.children[ch], word, idx+1)

Pattern 6: Longest Word in Dictionary

def longestWord(words: list[str]) -> str:
    """LC 720: Longest word where every prefix also in words"""
    trie = Trie()
    for w in sorted(words):  # sort for lexicographic preference
        trie.insert(w)

    result = ''
    def dfs(node, path):
        nonlocal result
        if len(path) > len(result):
            result = path
        for ch in sorted(node.children):
            child = node.children[ch]
            if child.is_end:  # only traverse valid prefix words
                dfs(child, path + ch)
    dfs(trie.root, '')
    return result

KMP Algorithm: String Pattern Matching

def kmp_search(text: str, pattern: str) -> list[int]:
    """Find all occurrences of pattern in text in O(n+m)"""
    def build_lps(p):
        """Longest Proper Prefix which is also Suffix"""
        lps = [0] * len(p)
        length = 0  # length of previous longest prefix suffix
        i = 1
        while i < len(p):
            if p[i] == p[length]:
                length += 1
                lps[i] = length
                i += 1
            elif length != 0:
                length = lps[length - 1]  # don't increment i
            else:
                lps[i] = 0
                i += 1
        return lps

    lps = build_lps(pattern)
    matches = []
    i = j = 0  # i = text index, j = pattern index
    while i < len(text):
        if text[i] == pattern[j]:
            i += 1; j += 1
        if j == len(pattern):
            matches.append(i - j)
            j = lps[j - 1]
        elif i < len(text) and text[i] != pattern[j]:
            if j != 0:
                j = lps[j - 1]
            else:
                i += 1
    return matches

Rabin-Karp: Rolling Hash

def rabin_karp(text: str, pattern: str) -> list[int]:
    """O(n+m) average, O(nm) worst case"""
    n, m = len(text), len(pattern)
    BASE, MOD = 31, 10**9 + 9

    def hash_str(s, length):
        h = 0
        for ch in s[:length]:
            h = (h * BASE + ord(ch) - ord('a') + 1) % MOD
        return h

    ph = hash_str(pattern, m)
    th = hash_str(text, m)
    power = pow(BASE, m - 1, MOD)
    matches = []

    for i in range(n - m + 1):
        if th == ph and text[i:i+m] == pattern:  # verify to avoid false positives
            matches.append(i)
        if i + m < n:
            th = (th - (ord(text[i]) - ord('a') + 1) * power) % MOD
            th = (th * BASE + ord(text[i+m]) - ord('a') + 1) % MOD
    return matches

Key Interview Tips

  • Trie vs HashMap: Use trie when prefix queries matter, or when you need ordered traversal of strings. HashMap is faster for exact lookups.
  • Memory optimization: Replace dict children with array of size 26 for lowercase-only problems — faster access, predictable memory.
  • Trie deletion: Decrement count at each node; remove node if count == 0 and is_end == False — avoids orphaned nodes.
  • KMP vs Rabin-Karp: KMP is O(n+m) worst case; Rabin-Karp is O(n+m) average but handles multiple patterns efficiently.
  • Aho-Corasick: KMP generalized to multiple patterns — build trie of patterns with failure links; matches all patterns in O(n + m_total + matches).

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “When should you use a trie instead of a hash map for string problems?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Use a trie when: (1) you need prefix queries (“does any word start with X?”) u2014 hash maps require iterating all keys; (2) you need ordered traversal of strings (lexicographic order comes for free from trie structure); (3) you’re matching multiple patterns simultaneously (Aho-Corasick builds on trie with failure links); (4) you need to count words sharing a prefix. Use hash maps when you only need exact lookups u2014 they’re O(1) vs trie’s O(L) with better constants. The trie’s overhead is memory: O(ALPHABET u00d7 N u00d7 L) nodes vs O(N) for a hash map.”
}
},
{
“@type”: “Question”,
“name”: “How does the KMP algorithm achieve O(n+m) string matching?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “KMP avoids redundant comparisons by precomputing the Longest Proper Prefix which is also a Suffix (LPS) array for the pattern. When a mismatch occurs at pattern position j, instead of restarting from the beginning, KMP falls back to lps[j-1] u2014 the length of the longest proper prefix of pattern[0..j-1] that matches a suffix. This means the text pointer i never moves backward: each character is examined at most twice (once forward, once when used in a fallback). Building the LPS takes O(m); matching takes O(n). Combined: O(n+m).”
}
},
{
“@type”: “Question”,
“name”: “What is the Aho-Corasick algorithm and when is it used?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Aho-Corasick generalizes KMP to simultaneously match multiple patterns in a single pass over the text. It builds a trie of all patterns, then adds failure links (like KMP’s LPS) between trie nodes u2014 when a character doesn’t match in the current node, follow the failure link to the longest proper suffix that is also a prefix in the trie. Time complexity: O(n + m_total + output) where n is text length, m_total is total pattern length, and output is total number of matches. Used in network intrusion detection (matching thousands of malware signatures), spam filtering, and multi-keyword search u2014 any problem requiring efficient multi-pattern matching.”
}
}
]
}

  • Shopify Interview Guide
  • Atlassian Interview Guide
  • LinkedIn Interview Guide
  • Apple Interview Guide
  • Uber Interview Guide
  • Companies That Ask This

    Scroll to Top