Pattern

Trie (Prefix Tree)

Store and search strings efficiently using a tree that shares common prefixes between words.

Roadmap

  1. What Is This Pattern?
  2. When to Use It
  3. Variant 1 — Basic Trie (Insert / Search / StartsWith)
  4. Variant 2 — Trie with Wildcards or Counting
  5. Template Code
  6. Visual Walkthrough
  7. Common Problems
  8. Interactive Demo
  9. Complexity Analysis
  10. Key Takeaways
Step 01

What Is This Pattern?

A trie (pronounced "try") is a tree-shaped data structure where every node represents a single character. Strings that share a common prefix share the same path from the root. This means inserting, searching, and prefix-matching all take O(L) time, where L is the length of the word — completely independent of how many words are stored.

The Core Idea

Each edge represents a character. A path from root to a marked node spells out a word. Shared prefixes are stored only once.

root
a
a
t
t
p
p
o
o
p
p*
p
p*
l
l
e
e*
Words stored: "app", "apple", "top"* = end of word
💡

Why not just use a hash set? A hash set can check if a word exists, but it cannot efficiently answer "what words start with this prefix?" A trie answers prefix queries in O(prefix length) and naturally supports autocomplete, spell-check, and wildcard matching — all operations that hash-based structures struggle with.

Step 02

When to Use It

Look for these recognition signals in the problem statement. If you spot one, a trie is very likely the intended data structure.

"Prefix matching" or "startsWith"
Check if any stored word begins with a given prefix. This is the classic trie operation.
"Autocomplete" or "search suggestions"
Given a prefix, return all words that start with it. Trie traversal collects them naturally.
"Word dictionary" with "wildcards"
Search for words where some characters are unknown (like '.'). DFS on a trie handles wildcard branches.
"Replace words" or "shortest prefix"
Find the shortest prefix of a word that exists in a dictionary. Walk the trie and stop at the first end-of-word marker.

The key clue: You need to store a dictionary of strings and perform prefix-based operations (prefix search, prefix replacement, or character-by-character matching). A brute-force approach checking every word is O(N * L). A trie reduces prefix lookups to O(L) regardless of dictionary size.

Step 03

Variant 1 — Basic Trie (Insert / Search / StartsWith)

Core Operations

The basic trie supports three operations: insert a word, search for an exact word, and check if any word starts with a given prefix. Each node has up to 26 children (for lowercase English letters) and an isEnd flag.

Walkthrough: insert("cat"), insert("car"), search("cat"), startsWith("ca")

Building a trie step by step, then querying it.

insert("cat")
Start at root. Create child c, then a, then t. Mark t as end-of-word.
c
a
t*
t*
a
c
insert("car")
Start at root. c already exists, a already exists. Create new child r. Mark it as end-of-word. The prefix "ca" is shared.
c
a
r*
t*
r*
a
c
search("cat")
Traverse root → cat. Node t has isEnd = true. Return true.
startsWith("ca")
Traverse root → ca. Node exists (regardless of isEnd). Return true.
Step 04

Variant 2 — Trie with Wildcards or Counting

Advanced Operations

Extend the basic trie with two powerful capabilities: wildcard search (matching '.' against any character) and prefix counting (tracking how many words pass through or end at each node).

Wildcard Search: search("c.t")

When we encounter a '.', we branch out to all children using DFS. If any branch leads to a match, return true.

char 'c': Exact match. Move to child 'c'.
char '.': Wildcard! Try every child of 'c'. We have 'a' → continue DFS for each.
char 't': In the 'a' branch: child 't' exists and isEnd = true. Match found!

Counting Variant: passCount & endCount

Each node stores two counters: passCount (how many words pass through this node) and endCount (how many words end here). This enables "count words with prefix" and "count exact matches."

insert("app"): root(pass:1) → a(pass:1) → p(pass:1) → p(pass:1, end:1)
insert("apple"): root(pass:2) → a(pass:2) → p(pass:2) → p(pass:2, end:1) → l(pass:1) → e(pass:1, end:1)
countPrefix("app"): Walk to node 'p' (second p). passCount = 2 (both "app" and "apple" pass through).
countWord("app"): Walk to node 'p' (second p). endCount = 1 (only "app" ends here).
🎯

Choosing the variant: Use the basic trie when you only need insert, search, and startsWith. Add wildcard DFS for pattern matching with unknown characters. Add counting fields when you need to know how many words share a prefix (e.g., Map Sum Pairs) or support deletion.

Step 05

Template Code

Here is the annotated Java template for a standard trie with insert, search, and startsWith.

Trie Node + Core Operations

class TrieNode {
    TrieNode[] children = new TrieNode[26];
    boolean isEnd = false;
}

class Trie {
    TrieNode root = new TrieNode();

    void insert(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (node.children[idx] == null)
                node.children[idx] = new TrieNode();  // create if missing
            node = node.children[idx];                // move down
        }
        node.isEnd = true;                          // mark end of word
    }

    boolean search(String word) {
        TrieNode node = traverse(word);
        return node != null && node.isEnd;          // must be end-of-word
    }

    boolean startsWith(String prefix) {
        return traverse(prefix) != null;            // just needs to exist
    }

    // Helper: walk down the trie following each char
    TrieNode traverse(String s) {
        TrieNode node = root;
        for (char c : s.toCharArray()) {
            int idx = c - 'a';
            if (node.children[idx] == null) return null;
            node = node.children[idx];
        }
        return node;
    }
}

Wildcard Search (DFS for '.' characters)

boolean searchWild(String word) {
    return dfs(root, word, 0);
}

boolean dfs(TrieNode node, String word, int i) {
    if (i == word.length()) return node.isEnd;

    char c = word.charAt(i);
    if (c == '.') {
        // wildcard: try every non-null child
        for (TrieNode child : node.children)
            if (child != null && dfs(child, word, i + 1))
                return true;
        return false;
    } else {
        int idx = c - 'a';
        if (node.children[idx] == null) return false;
        return dfs(node.children[idx], word, i + 1);
    }
}
📝

Array vs HashMap children. Using TrieNode[26] gives O(1) child access and is fine when the character set is small (lowercase English). For larger alphabets (Unicode), use a HashMap<Character, TrieNode> to avoid wasting memory on empty slots.

Step 06

Visual Walkthrough

Let us trace through building a trie and searching it, showing the tree structure at every stage.

Words: ["tea", "ten", "to", "inn"]

Building the trie one word at a time, then performing lookups.

OPERATION ACTION TRIE PATHS NODES CREATED
insert("tea") Create t → e → a* t-e-a* 3 new
insert("ten") Reuse t → e, create n* t-e-a* | t-e-n* 1 new
insert("to") Reuse t, create o* t-e-a* | t-e-n* | t-o* 1 new
insert("inn") New branch: i → n → n* t-e-a* | t-e-n* | t-o* | i-n-n* 3 new
search("tea") Walk t → e → a, isEnd? true (a has isEnd=true) -
search("te") Walk t → e, isEnd? false (e has isEnd=false) -
startsWith("te") Walk t → e, exists? true (node exists) -
Total nodes created: 8 (not 11). Shared prefixes saved 3 nodes.
💡

Notice the savings: "tea" and "ten" share the prefix "te", so those two characters are stored once, not twice. As the dictionary grows, more prefixes overlap. This is what makes tries space-efficient for large dictionaries with common prefixes.

Step 07

Common Problems

These are the classic LeetCode problems that use the trie pattern, listed in rough order of difficulty.

#208 Implement Trie (Prefix Tree) Medium
#720 Longest Word in Dictionary Medium
#648 Replace Words Medium
#677 Map Sum Pairs Medium
#211 Design Add and Search Words Data Structure Medium
#212 Word Search II Hard
💡

Practice tip: Start with #208 (direct implementation of the template). Then try #648 (walk the trie to find shortest matching prefix for each word). Once comfortable, tackle #211 (wildcard DFS) and #212 (trie + backtracking on a 2D board, one of the most popular hard trie problems).

Step 08

Interactive Demo

Type words to insert into a trie and watch the tree structure build visually. Then search for words or prefixes to see the traversal in action.

⚙ Trie Visualizer




Press "Batch Insert" or type a word and press "Run" to build the trie.
Insert words to build the trie, then search or check prefixes.
Step 09

Complexity Analysis

Insert / Search / Prefix
O(L)
Space
O(N * L)

Why O(L) per Operation?

Each operation (insert, search, startsWith) traverses at most L characters, where L is the length of the word or prefix. At each character, the work is O(1): check or create a child in a fixed-size array. Total: O(L) per operation — completely independent of dictionary size N.

Space: In the worst case (no shared prefixes), we store one node per character across all words. If N words each have average length L, the space is O(N * L). In practice, shared prefixes reduce this significantly.

Wildcard search: In the worst case with many '.' characters, the wildcard DFS may explore all branches, giving O(26^L) for a word of all dots. In practice, specific characters prune the search heavily.

Trie vs hash map for prefix operations: A hash set gives O(L) for exact search but O(N * L) for "does any word start with X?" (you must check every word). A trie gives O(L) for both. This is the fundamental advantage of the trie data structure.

Step 10

Key Takeaways

01

Each node is a character, each path is a word. A trie encodes strings as paths from root to end-of-word nodes. Shared prefixes share nodes, making storage efficient for dictionaries with common beginnings.

02

O(L) for insert, search, and prefix lookup. All three core operations are proportional to word length, not dictionary size. This is what makes tries ideal for prefix-heavy workloads like autocomplete and spell checking.

03

search() checks isEnd, startsWith() does not. The only difference between searching for an exact word and checking a prefix is the final isEnd flag. Both traverse the trie identically.

04

Wildcards use DFS branching at '.' characters. When a wildcard is encountered, recursively explore all children. For exact characters, follow the single matching child. This naturally extends the trie to support regex-like patterns.

05

Combines powerfully with backtracking and BFS. In problems like Word Search II (#212), a trie prunes the backtracking search on a 2D grid: instead of checking each word separately, you walk the trie as you explore the board, eliminating entire branches when the prefix does not exist.