Trie Interview Patterns

Trie (prefix tree) problems appear frequently in coding interviews. This guide covers the core patterns with Python implementations you can adapt to any trie problem.

Trie Node Structure and Basic Operations (LC 208)

Every trie implementation starts with a node class and the three core operations: insert, search, and startsWith.

class TrieNode:
    def __init__(self):
        self.children = {}  # char -> TrieNode
        self.is_end = False

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.is_end = True

    def search(self, word: str) -> bool:
        node = self.root
        for ch in word:
            if ch not in node.children:
                return False
            node = node.children[ch]
        return node.is_end

    def startsWith(self, prefix: str) -> bool:
        node = self.root
        for ch in prefix:
            if ch not in node.children:
                return False
            node = node.children[ch]
        return True

Time: O(m) for all operations where m is the word length. Space: O(m * n) total for n words of average length m.

Word Dictionary with Wildcards (LC 211)

When ‘.’ can match any character, use DFS through the trie trying all children at wildcard positions.

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

    def addWord(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.is_end = True

    def search(self, word: str) -> bool:
        return self._dfs(word, 0, self.root)

    def _dfs(self, word: str, i: int, node: TrieNode) -> bool:
        if i == len(word):
            return node.is_end
        ch = word[i]
        if ch == '.':
            # Try all children
            for child in node.children.values():
                if self._dfs(word, i + 1, child):
                    return True
            return False
        else:
            if ch not in node.children:
                return False
            return self._dfs(word, i + 1, node.children[ch])

Word Search II (LC 212) – DFS on Board with Trie Pruning

Build a trie from all words, then DFS from every board cell. The key optimization: remove matched words from the trie to avoid duplicate results and speed up subsequent searches.

class TrieNodeWS:
    def __init__(self):
        self.children = {}
        self.word = None  # store complete word at end node

class Solution:
    def findWords(self, board, words):
        root = TrieNodeWS()
        for w in words:
            node = root
            for ch in w:
                if ch not in node.children:
                    node.children[ch] = TrieNodeWS()
                node = node.children[ch]
            node.word = w

        rows, cols = len(board), len(board[0])
        results = []

        def dfs(r, c, node):
            ch = board[r][c]
            if ch not in node.children:
                return
            next_node = node.children[ch]
            if next_node.word:
                results.append(next_node.word)
                next_node.word = None  # remove to avoid duplicates

            board[r][c] = '#'  # mark visited
            for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
                nr, nc = r+dr, c+dc
                if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != '#':
                    dfs(nr, nc, next_node)
            board[r][c] = ch  # restore

            # Prune: remove leaf nodes with no remaining words
            if not next_node.children and not next_node.word:
                del node.children[ch]

        for r in range(rows):
            for c in range(cols):
                dfs(r, c, root)

        return results

The pruning step (deleting empty leaf nodes) is what separates a solution that times out from one that passes. After finding a word, its trie path becomes dead weight – remove it.

Auto-Complete System Design

A production auto-complete system uses a trie augmented with frequency counts and top-k results cached at each node.

import heapq

class AutocompleteNode:
    def __init__(self):
        self.children = {}
        self.freq = 0
        # Cache top-3 (word, freq) pairs at this node
        self.top_k = []  # min-heap by freq

class AutocompleteSystem:
    def __init__(self, sentences, times):
        self.root = AutocompleteNode()
        self.current = []  # characters typed so far
        for s, t in zip(sentences, times):
            self._insert(s, t)

    def _insert(self, sentence: str, freq: int):
        node = self.root
        for ch in sentence:
            if ch not in node.children:
                node.children[ch] = AutocompleteNode()
            node = node.children[ch]
            # Update top-k at each node along the path
            self._update_topk(node, sentence, freq)
        node.freq += freq

    def _update_topk(self, node, sentence, freq):
        # Keep a min-heap of size 3
        for i, (f, s) in enumerate(node.top_k):
            if s == sentence:
                node.top_k[i] = (freq, sentence)
                heapq.heapify(node.top_k)
                return
        if len(node.top_k)  node.top_k[0][0]:
            heapq.heapreplace(node.top_k, (freq, sentence))

    def input(self, c: str):
        if c == '#':
            sentence = ''.join(self.current)
            self._insert(sentence, 1)
            self.current = []
            return []
        self.current.append(c)
        node = self.root
        for ch in self.current:
            if ch not in node.children:
                return []
            node = node.children[ch]
        return [s for f, s in sorted(node.top_k, reverse=True)]

Design notes: For real systems, the top-k cache makes each prefix lookup O(1) instead of O(subtree size). Trade-off: inserts become slower because you update cached results at every prefix node. With billions of queries, precompute top-k offline and store in Redis.

Bitwise Trie for XOR Maximum (LC 421)

Insert numbers bit by bit from MSB (bit 31) to LSB (bit 0). To find the number that maximizes XOR with a query, greedily go to the opposite bit at each level.

class BitwiseTrieNode:
    def __init__(self):
        self.children = [None, None]  # index 0 or 1

class Solution:
    def findMaximumXOR(self, nums) -> int:
        root = BitwiseTrieNode()

        def insert(num):
            node = root
            for i in range(31, -1, -1):
                bit = (num >> i) & 1
                if not node.children[bit]:
                    node.children[bit] = BitwiseTrieNode()
                node = node.children[bit]

        def query(num) -> int:
            node = root
            xor = 0
            for i in range(31, -1, -1):
                bit = (num >> i) & 1
                want = 1 - bit  # go opposite to maximize XOR
                if node.children[want]:
                    xor |= (1 << i)
                    node = node.children[want]
                else:
                    node = node.children[bit]
            return xor

        for n in nums:
            insert(n)

        return max(query(n) for n in nums)

This runs in O(32n) = O(n) time. The bitwise trie has at most 32 * n nodes for 32-bit integers.

Replace Words with Root (LC 648)

Build a trie from all roots. For each word in the sentence, walk the trie – if you hit an end node before consuming the full word, replace with the root.

class Solution:
    def replaceWords(self, dictionary, sentence: str) -> str:
        root = TrieNode()
        for word in dictionary:
            node = root
            for ch in word:
                if ch not in node.children:
                    node.children[ch] = TrieNode()
                node = node.children[ch]
            node.is_end = True

        def replace(word):
            node = root
            for i, ch in enumerate(word):
                if ch not in node.children:
                    return word  # no root found
                node = node.children[ch]
                if node.is_end:
                    return word[:i+1]  # replace with root
            return word

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

Pattern Recognition: Trie vs Hash Set

Use a trie when the problem involves prefix queries. Use a hash set when you need exact membership checks.

Signal in problemData structure
“starts with”, “prefix”, “autocomplete”Trie
“common prefix”, “replace by shortest prefix”Trie
“find all words matching pattern”Trie + DFS
“exists in set”, “seen before”Hash set
“maximize XOR”Bitwise trie
No prefix queries neededHash set (simpler, faster)

Key trie trade-offs: faster prefix queries than hash set, but higher memory usage (each character is a node). For small alphabets (26 letters) this is fine. For Unicode, use a hash map for children instead of a fixed array.

Meta coding interviews frequently test trie problems. See common patterns for Meta interview: trie and string search problems.

Apple coding rounds include trie and string prefix problems. See patterns for Apple interview: trie and autocomplete system design.

LinkedIn engineering interviews test trie and autocomplete patterns. See common questions for LinkedIn interview: trie and search problems.

Scroll to Top