Trie
O(m) SpaceO(n * m) Think of autocomplete on your phone. As you type “h-e-l,” the system instantly narrows to words starting with “hel” — hello, help, helmet. A trie stores words letter by letter along branching paths, so shared prefixes share nodes. Typing each letter just follows one edge.
When to use it: Prefix matching, autocomplete, spell checking, word search in a grid, or any problem that asks “does any word start with this prefix?”
Key insight: Each node represents a character, and the path from root to a node spells out a prefix. Lookup time depends only on the length of the query string, not the number of stored words.
Common trap: Excessive memory usage. Each node may have up to 26 children (for lowercase English). For sparse tries, use a hash map instead of an array at each node.
class TrieNode:
children = {} # char -> TrieNode
isEndOfWord = false
class Trie:
root = new TrieNode()
function insert(word):
node = root
for char in word:
if char not in node.children:
node.children[char] = new TrieNode()
node = node.children[char]
node.isEndOfWord = true # mark end of complete word
function search(word):
node = root
for char in word:
if char not in node.children:
return false
node = node.children[char]
return node.isEndOfWord # must be a complete word, not just prefix
function startsWith(prefix):
node = root
for char in prefix:
if char not in node.children:
return false
node = node.children[char]
return true # prefix exists class TrieNode:
def __init__(self):
self.children: dict[str, "TrieNode"] = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
def search(self, word: str) -> bool:
node = self._find_node(word)
return node is not None and node.is_end
def starts_with(self, prefix: str) -> bool:
return self._find_node(prefix) is not None
def _find_node(self, prefix: str) -> TrieNode | None:
node = self.root
for char in prefix:
if char not in node.children:
return None
node = node.children[char]
return node
def words_with_prefix(self, prefix: str) -> list[str]:
"""Return all words that start with the given prefix."""
node = self._find_node(prefix)
if not node:
return []
results = []
self._collect(node, list(prefix), results)
return results
def _collect(self, node: TrieNode, path: list[str], results: list[str]) -> None:
if node.is_end:
results.append("".join(path))
for char, child in node.children.items():
path.append(char)
self._collect(child, path, results)
path.pop() Problem: Design a data structure that supports adding words and searching for words, where searches can contain the wildcard character '.' that matches any single letter.
How would you approach this?
Answer: This is a trie problem. Insert words normally. For search, when encountering '.', recursively try all children at that node. The trie structure makes this efficient because you only branch at wildcard positions.
Problem: Given a 2D board of characters and a list of words, find all words that can be formed by sequentially adjacent cells (up/down/left/right).
How would you approach this?
Answer: This is trie + DFS backtracking. Build a trie from the word list, then DFS from each cell. The trie lets you prune paths early — if no word in the list starts with the current prefix, stop exploring that direction.
Time: O(m) per operation (insert, search, startsWith) where m is the length of the word or prefix. Each character requires one hash map lookup and one step down the tree.
Space: O(n * m) in the worst case, where n is the number of words and m is the average word length. Each character in each word may create a new node. In practice, shared prefixes reduce this significantly.