Store and search strings efficiently using a tree that shares common prefixes between words.
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.
Each edge represents a character. A path from root to a marked node spells out a word. Shared prefixes are stored only once.
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.
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"
"Autocomplete" or "search suggestions"
"Word dictionary" with "wildcards"
"Replace words" or "shortest prefix"
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.
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.
Building a trie step by step, then querying it.
isEnd = true. Return true.isEnd). Return true.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).
When we encounter a '.', we branch out to all children using DFS. If any branch leads to a match, return true.
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."
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.
Here is the annotated Java template for a standard trie with insert, search, and startsWith.
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; } }
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.
Let us trace through building a trie and searching it, showing the tree structure at every stage.
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) | - |
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.
These are the classic LeetCode problems that use the trie pattern, listed in rough order of difficulty.
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).
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.
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.
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.
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.
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.
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.
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.