HomeBlog › The Trie (Prefix Tree) Pattern

The Trie (Prefix Tree) Pattern for Coding Interviews

The go-to structure for prefix problems. Node layout, insert/search/startsWith, a clean Python TrieNode, and three worked examples with Big-O.

A trie — also called a prefix tree — stores a set of strings by sharing their common prefixes along a tree of characters. Each edge is one character, and a path from the root spells out a prefix. It turns "does any word start with ca?" into an O(L) walk down the tree, which is why it shows up in autocomplete, spell-checkers, and word-search puzzles.

This guide covers the trie data structure end to end for the trie coding interview: the node layout, insert/search/startsWith, when to reach for it, a clean Python implementation, and three worked problems with complexity.

The structure

A trie is made of nodes. Each node holds a map from a character to a child node, plus a boolean flag marking whether a word ends at that node. The root represents the empty prefix. Walking c → a → t and finding the flag set at the last node means "cat" was inserted; if the flag is unset there, "cat" is only a prefix of some longer word.

Key idea: the end-of-word flag is what separates a stored word from a mere prefix. Skip it and search("ca") will wrongly return true just because "cat" exists.

When to recognize this pattern

Trie vs a hash set of words

A hash set answers exact membership in O(L), but it is blind to prefixes: to ask "how many words start with ca?" you would scan every key. A trie shares prefixes structurally, so startsWith is O(L) and you can enumerate every word under a prefix by walking the subtree. In an interview, the tell is repetition: if the same dictionary is queried many times, or if you need to answer prefix questions rather than exact lookups, the trie's one-time build cost pays for itself and every later query drops to the length of the key.

OperationHash set of wordsTrie
Exact searchO(L)O(L)
Prefix check (startsWith)O(total chars)O(L)
Enumerate words with a prefixFull scanSubtree walk
SpaceO(total chars)O(total chars), shared prefixes save some

A clean Python implementation

This is the standard "Implement Trie" answer — a TrieNode with a children dict and an is_word flag, plus insert/search/startsWith.

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

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

    def insert(self, word):
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_word = True

    def _walk(self, prefix):
        node = self.root
        for ch in prefix:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

    def search(self, word):
        node = self._walk(word)
        return node is not None and node.is_word

    def startsWith(self, prefix):
        return self._walk(prefix) is not None

Each operation is O(L) in the key length, independent of how many words the trie holds. Space is O(total characters inserted).

Worked examples

Add and search word (with wildcards)

Problem: Support addWord and search, where a search string may contain . matching any single character.

Insert is the standard walk. Search becomes a DFS: on a ., recurse into every child.

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

    def addWord(self, word):
        node = self.root
        for ch in word:
            node = node.children.setdefault(ch, TrieNode())
        node.is_word = True

    def search(self, word):
        def dfs(node, i):
            if i == len(word):
                return node.is_word
            ch = word[i]
            if ch == '.':
                return any(dfs(child, i + 1) for child in node.children.values())
            return ch in node.children and dfs(node.children[ch], i + 1)
        return dfs(self.root, 0)

O(L) for a concrete word; a wildcard-heavy query can branch up to O(26L) in the worst case, but real inputs stay close to linear.

Word Search II

Problem: Given a grid of letters and a word list, return every word found by connecting adjacent cells.

Build a trie of the word list, then DFS the grid once, pruning any branch whose prefix isn't in the trie. Marking a node's word and removing it after a hit avoids duplicates.

def findWords(board, words):
    root = TrieNode()
    for w in words:
        node = root
        for ch in w:
            node = node.children.setdefault(ch, TrieNode())
        node.word = w                      # store full word at the end

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

    def dfs(r, c, node):
        ch = board[r][c]
        nxt = node.children.get(ch)
        if not nxt:
            return
        if getattr(nxt, 'word', None):
            found.append(nxt.word)
            nxt.word = None                # avoid duplicates
        board[r][c] = '#'
        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, nxt)
        board[r][c] = ch                   # restore

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

The trie is what makes this tractable: one grid traversal checks every word at once and abandons dead prefixes immediately, instead of re-searching per word.

Replace Words

Problem: Given a dictionary of roots and a sentence, replace each word by the shortest root that is a prefix of it.

Insert all roots, then for each word walk the trie and stop at the first end-of-word node.

def replaceWords(dictionary, sentence):
    root = TrieNode()
    for w in dictionary:
        node = root
        for ch in w:
            node = node.children.setdefault(ch, TrieNode())
        node.is_word = True

    def shortest_root(word):
        node = root
        prefix = []
        for ch in word:
            if ch not in node.children:
                break
            node = node.children[ch]
            prefix.append(ch)
            if node.is_word:
                return ''.join(prefix)
        return word

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

O(total characters) across all roots and words. Returning at the first is_word guarantees the shortest matching root.

Common pitfalls

Reach for the right data structure, live

CoPilot Interview is a desktop AI interview assistant that recognizes when a problem is a trie, sliding window, or graph — and shows working code with Big-O in about 4 seconds during real Zoom and Teams calls. Free tier for Windows and macOS.

See the coding interview copilot

FAQ

When should I use a trie?

Reach for a trie when the problem is about prefixes over a set of strings — autocomplete, prefix matching, word dictionaries with wildcard search, or scanning a grid for many words at once. If you find yourself checking whether many strings share a starting sequence, a trie usually beats a hash set.

How does a trie differ from a hash set of words?

A hash set answers exact-membership in O(L) but knows nothing about prefixes. A trie shares common prefixes across words, so it answers startsWith in O(L) and lets you enumerate every word under a prefix, which a hash set cannot do without scanning everything.

What is the time complexity of trie operations?

Insert, search, and startsWith each run in O(L) where L is the length of the key, independent of how many words the trie holds. Space is O(total characters) in the worst case, though shared prefixes reduce it in practice.

Why is a trie faster than a hash set for Word Search II?

A trie lets a single DFS over the grid check all target words at once and prune a branch the moment no word shares the current prefix. A hash set would force you to search the grid separately for each word, which is far slower.

What is the most common trie bug?

Forgetting the end-of-word marker. Without a boolean flag on the terminal node, search cannot distinguish a full word from a mere prefix, so it returns true for prefixes that were never actually inserted.