Trie Data Structure for String Problems: A Practical Interview Guide

Learn how tries solve prefix-heavy string problems like autocomplete and word search, with Python examples, complexity analysis, and interview tips.

trie
prefix tree
autocomplete
word search
interview prep

Why tries show up so often in interviews

If a problem asks you to match words by prefix, search a dictionary efficiently, or explore many string branches at once, a trie is often the cleanest tool for the job. Interviewers like tries because they test whether you can turn repeated string comparisons into structured traversal.

A trie, also called a prefix tree, stores strings character by character. That sounds simple, but it unlocks powerful patterns:

  • Autocomplete: find all words that start with a prefix.
  • Word search: scan a board while walking a dictionary at the same time.
  • Prefix queries: test whether any word starts with a given string.
  • Dictionary operations: insert, search, and delete words efficiently.

If you know the core idea, trie problems become much less mysterious. The trick is to stop thinking about whole words and start thinking about shared prefixes.

Problem statement: when a hash set is not enough

Suppose you have a list of words and you need to answer questions like:

  • Does this exact word exist?
  • Is there any word that starts with this prefix?
  • What words can I reach from this partial string?

A hash set handles exact lookup well, but it does not naturally support prefix traversal. You could scan every word for every query, but that becomes expensive fast.

A trie organizes words so shared prefixes are stored once. For example, with:

  • cat
  • car
  • care
  • dog

The words cat, car, and care share the path c -> a -> .... That shared structure is what makes the trie useful.

Intuition: store prefixes as paths

Each node in a trie represents a prefix. Each edge corresponds to one character. A node usually contains:

  • a map from character to next node
  • a boolean flag like is_word to mark complete words

Think of it like walking down a tree:

  • root = empty prefix
  • first level = one-letter prefixes
  • second level = two-letter prefixes
  • and so on

This gives you two major benefits:

  1. Prefix queries are fast because you follow characters directly.
  2. Multiple words with shared prefixes reuse nodes, saving repeated work.

The tradeoff is memory. Tries are often faster for prefix-heavy tasks, but they use more space than a simple list or set.

Core trie implementation in Python

Let’s build a basic trie with three operations:

  • insert(word)
  • search(word)
  • starts_with(prefix)
python
class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_word = 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_word = 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_word

    def starts_with(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

Worked example 1: basic insert and search

python
trie = Trie()
trie.insert("cat")
trie.insert("car")
trie.insert("care")

print(trie.search("car"))     # True
print(trie.search("ca"))      # False
print(trie.starts_with("ca")) # True
print(trie.starts_with("cap")) # False

What happens internally

When you insert cat, the trie creates nodes for c, a, t, then marks t as a word end.

When you insert car, it reuses c -> a, then creates r.

When you insert care, it reuses c -> a -> r, then creates e.

That reuse is the whole point. You are compressing repeated prefixes into a shared structure.

Step-by-step approach for interview problems

Most trie interview problems follow the same pattern:

1. Build the trie from the input dictionary

If the problem gives you a word list, insert each word into the trie first.

2. Traverse the trie while processing the query

For prefix lookup, walk the trie character by character.

For backtracking problems like word search, combine trie traversal with DFS on the board.

3. Use is_word to detect completed matches

A prefix is not always a full word. Marking word ends avoids false positives.

4. Prune dead branches when possible

If a subtree can no longer lead to a solution, stop exploring it. This is especially useful in backtracking problems.

Worked example 2: autocomplete suggestions

A common interview task is to return all words matching a prefix.

Here is a helper that finds the node for a prefix, then collects all words below it.

python
class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_word = 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_word = True

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

    def autocomplete(self, prefix: str):
        node = self._find_node(prefix)
        if not node:
            return []

        results = []

        def dfs(curr, path):
            if curr.is_word:
                results.append(prefix + ''.join(path))
            for ch, nxt in curr.children.items():
                path.append(ch)
                dfs(nxt, path)
                path.pop()

        dfs(node, [])
        return results

Example usage

python
trie = Trie()
for word in ["cat", "car", "care", "cart", "dog"]:
    trie.insert(word)

print(trie.autocomplete("car"))
# ['car', 'care', 'cart']

Why this works

You first walk to the node that represents car. From there, every word in that subtree starts with car.

This is a common interview move: use the trie to narrow the search space, then use DFS to enumerate matches.

Worked example 3: word search with a trie

One of the most important trie applications is word search on a board. Given a 2D grid of letters and a list of words, find all words that can be formed by moving horizontally or vertically.

A brute-force approach checks every word against every path, which is too slow. A trie lets you search all words at once.

Approach

  • Insert all target words into a trie.
  • Start DFS from each board cell.
  • As you move, follow the trie branch for the current character.
  • If a path is not in the trie, stop immediately.
  • If you reach a node marked is_word, record the word.

Python code

python
class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_word = False
        self.word = None  # store full word at terminal 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.is_word = True
        node.word = word


def find_words(board, words):
    trie = Trie()
    for word in words:
        trie.insert(word)

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

    def dfs(r, c, node):
        ch = board[r][c]
        if ch not in node.children:
            return

        nxt = node.children[ch]
        if nxt.is_word:
            result.append(nxt.word)
            nxt.is_word = False  # 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, nxt)
        board[r][c] = ch

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

    return result

Example usage

python
board = [
    ['o', 'a', 'a', 'n'],
    ['e', 't', 'a', 'e'],
    ['i', 'h', 'k', 'r'],
    ['i', 'f', 'l', 'v']
]
words = ["oath", "pea", "eat", "rain"]

print(find_words(board, words))
# ['oath', 'eat']

Why the trie helps here

Without a trie, you would repeatedly attempt to match each word from each cell. With a trie, you only explore paths that match some dictionary prefix.

That pruning is a major performance improvement.

Complexity analysis

Trie complexity depends on the total length of inserted words, not just the number of words.

Basic trie operations

OperationTime ComplexitySpace ComplexityNotes
Insert wordO(L)O(L)L is word length, new nodes may be created
Search wordO(L)O(1)Walk the characters once
Prefix checkO(P)O(1)P is prefix length
AutocompleteO(P + M)O(M)M depends on number of returned characters/words

Word search comparison

ApproachTime ComplexitySpace ComplexityNotes
Check each word separately with DFSVery large, often O(W * R * C * 4^K)O(K)W = number of words, K = word length
Trie + DFS on boardPruned significantly in practiceO(total trie size + recursion)Usually the preferred interview solution

The key idea is that the trie reduces duplicate work across words with shared prefixes.

Common pitfalls

1. Confusing prefix with full word

starts_with("car") may be true even if car itself is not a valid word unless is_word is set.

2. Forgetting to mark visited cells in board search

In word search, you must avoid revisiting the same cell in one path.

3. Not pruning duplicates

If the same word can be found multiple ways, you may need to deduplicate results.

4. Overcomplicating the node structure

For many problems, a dictionary of children and one terminal flag are enough. Keep the node simple unless you need extra metadata.

When to reach for a trie

A trie is a strong fit when the problem has one or more of these traits:

  • lots of shared prefixes
  • repeated prefix lookups
  • dictionary-style search
  • autocomplete or suggestion ranking
  • grid/backtracking combined with a word list

A trie is usually not the first choice if you only need exact membership checks. A set is simpler and often faster for that case.

Related patterns and variations

Once you understand the trie, you will notice several common interview variations:

  • Map-based trie nodes for general alphabets
  • Array-based trie nodes for lowercase English letters
  • Compressed trie / radix tree for space savings
  • Prefix counting for problems like “how many words start with this prefix?”
  • Longest common prefix using a trie or incremental scanning

Key takeaways

  • A trie stores strings as shared prefix paths.
  • It makes prefix search and autocomplete efficient.
  • It is especially useful when multiple words share structure.
  • In backtracking problems like word search, a trie lets you prune invalid paths early.
  • Keep the implementation simple: children map plus terminal flag is enough for many problems.

Practice these next

Try these problems after you are comfortable with the basic trie pattern:

  1. Implement Trie (Prefix Tree)
  2. Add and Search Word
  3. Word Search II
  4. Replace Words
  5. Longest Common Prefix

If you want to see the trie traversal and pruning happen step by step, I built an interactive version of this walkthrough at codebrew.ai — might help if you are a visual learner.

Practice this pattern interactively

Generate a step-by-step lesson for any LeetCode problem and watch the algorithm run, variable by variable.

Start a free lesson

Related posts