Tries
A trie (prefix tree) stores strings character-by-character in a tree structure, enabling O(m) insert, search, and prefix lookup where m is the word length. Master basic trie operations, wildcard DFS, and trie-accelerated grid search.
Learn & Reference
Understanding the Pattern
Tries Pattern
Tries (pronounced "try", from retrieval) are tree structures where each node represents a character. A path from root to a marked node spells out a stored word. Unlike hash sets that treat words as opaque keys, tries expose the prefix structure of your dictionary.
Insert: apple, app, ape
root
|
a
|
p
/ \
p e ✓
/ \
l ✓ (app)
|
e ✓ (apple)
The key insight: shared prefixes share nodes. Searching for any word starting with "ap" only traverses 2 nodes, regardless of dictionary size.
The TrieNode
Every trie implementation starts with the same building block:
TrieNode:
children: map/array of child nodes (one per character)
isEnd: boolean — does a word end at this node?
Two common representations:
- Array
[26]: Fixed-size, O(1) lookup, best when input is lowercase English only - HashMap: Flexible, supports any character set (Unicode, mixed case)
Three Core Techniques
1. Basic Trie — Insert, Search, StartsWith
Insert: Walk the trie character by character. If a child doesn't exist, create it. Mark the last node as isEnd = true.
Search: Walk the trie. If any child is missing, return false. If you reach the end, check isEnd.
StartsWith: Same as search, but don't check isEnd — reaching the end of the prefix is enough.
insert("apple"): root → a → p → p → l → e✓
search("apple"): root → a → p → p → l → e✓ → isEnd? YES → true
search("app"): root → a → p → p✗ → isEnd? NO → false
startsWith("app"): root → a → p → p → node exists → true
2. Trie + DFS (Wildcard Search)
When the search pattern contains wildcards (e.g., . matches any character), you can't follow a single path. Instead, at each wildcard, branch into all existing children via DFS.
search("b.d"):
b → branch on '.' → try all children: a, then recurse
a → d → isEnd? YES → true
Time complexity rises to O(26^k) in the worst case where k = number of wildcards, but it's fast in practice because most branches terminate quickly.
3. Trie + Backtracking (Grid Word Search)
For finding multiple words in a 2D grid, build a trie from the word list first. Then DFS from each cell, following the trie to prune branches early. This is dramatically faster than running a separate DFS for each word.
Build trie from ["oath", "eat"]
DFS from each cell, following trie nodes:
cell 'o' → trie has 'o' child → continue
cell 'a' → trie has 'a' child → continue
cell 't' → trie has 't' child → continue
cell 'h' → trie has 'h' child, isEnd → found "oath"!
Critical optimization: After finding a word, remove it from the trie (set isEnd = false or prune the node) to avoid collecting duplicates.
Common Mistakes
- Forgetting
isEnd— A prefix like "app" exists in the trie if "apple" was inserted, butsearch("app")should return false unless "app" was explicitly inserted. Always checkisEnd. - Not handling wildcard
.— When.appears, you must try ALL children, not skip the character. This requires DFS/recursion. - Off-by-one in trie depth — String index
icorresponds to trie depthi+1(root is depth 0, first character is depth 1). - Duplicate results in Word Search II — If the same word can be found via multiple grid paths, you'll add it multiple times. Remove or mark found words in the trie.
- Array vs HashMap — Using
children[26]when input includes uppercase or special characters causes index-out-of-bounds. Use a HashMap for arbitrary character sets. - Not pruning dead branches — In Word Search II, after finding a word, if a trie node has no remaining children, prune it to speed up future searches.
Template
1class TrieNode:2def __init__(self):3self.children = {}4self.is_end = False56class Trie:7def __init__(self):8self.root = TrieNode()910def insert(self, word):11node = self.root12for char in word:13if char not in node.children:14node.children[char] = TrieNode()15node = node.children[char]16node.is_end = True1718def search(self, word):19node = self.root20for char in word:21if char not in node.children:22return False23node = node.children[char]24return node.is_end2526def starts_with(self, prefix):27node = self.root28for char in prefix:29if char not in node.children:30return False31node = node.children[char]32return True
Syntax Reference
1# --- TrieNode ---2class TrieNode:3def __init__(self):4self.children = {} # char -> TrieNode5self.is_end = False67# --- Insert ---8node = root9for char in word:10if char not in node.children:11node.children[char] = TrieNode()12node = node.children[char]13node.is_end = True1415# --- Search ---16node = root17for char in word:18if char not in node.children:19return False20node = node.children[char]21return node.is_end2223# --- Starts With ---24node = root25for char in prefix:26if char not in node.children:27return False28node = node.children[char]29return True # Don't check is_end
Common Recipes
1# Recipe 1: Basic Trie — Insert / Search / StartsWith2class TrieNode:3def __init__(self):4self.children = {}5self.is_end = False67class Trie:8def __init__(self):9self.root = TrieNode()1011def insert(self, word):12node = self.root13for char in word:14if char not in node.children:15node.children[char] = TrieNode()16node = node.children[char]17node.is_end = True1819def search(self, word):20node = self.root21for char in word:22if char not in node.children:23return False24node = node.children[char]25return node.is_end2627def starts_with(self, prefix):28node = self.root29for char in prefix:30if char not in node.children:31return False32node = node.children[char]33return True3435# Recipe 2: Wildcard Search with DFS36def search_with_wildcard(node, word, i):37if i == len(word):38return node.is_end39if word[i] == '.':40for child in node.children.values():41if search_with_wildcard(child, word, i + 1):42return True43return False44if word[i] not in node.children:45return False46return search_with_wildcard(node.children[word[i]], word, i + 1)4748# Recipe 3: Grid Word Search with Trie49def find_words(board, words):50trie = Trie()51for w in words:52trie.insert(w)53result = []54def dfs(r, c, node, path):55if node.is_end:56result.append(path)57node.is_end = False # Avoid duplicates58if r < 0 or r >= len(board) or c < 0 or c >= len(board[0]):59return60char = board[r][c]61if char not in node.children:62return63board[r][c] = '#'64for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)]:65dfs(r+dr, c+dc, node.children[char], path+char)66board[r][c] = char67for r in range(len(board)):68for c in range(len(board[0])):69dfs(r, c, trie.root, "")
Complexity
| Technique | Insert | Search | Space | Example |
|---|---|---|---|---|
| Basic Trie | O(m) | O(m) | O(n*m) | Implement Trie |
| Trie + DFS | O(m) | O(26^k * m) | O(n*m) | Add & Search Words |
| Trie + Backtracking | O(n*m) | O(R*C*4^L) | O(n*m) | Word Search II |
Watch Out
- ✗Forgetting `isEnd` — A prefix like "app" exists in the trie if "apple" was inserted, but `search("app")` should return false unless "app" was explicitly inserted. Always check `isEnd`.
- ✗Not handling wildcard `.` — When `.` appears, you must try ALL children, not skip the character. This requires DFS/recursion.
- ✗Off-by-one in trie depth — String index `i` corresponds to trie depth `i+1` (root is depth 0, first character is depth 1).
- ✗Duplicate results in Word Search II — If the same word can be found via multiple grid paths, you'll add it multiple times. Remove or mark found words in the trie.
- ✗Array vs HashMap — Using `children[26]` when input includes uppercase or special characters causes index-out-of-bounds. Use a HashMap for arbitrary character sets.
- ✗Not pruning dead branches — In Word Search II, after finding a word, if a trie node has no remaining children, prune it to speed up future searches.