Tries: Prefix Trees for Fast String Operations
Build a Trie from scratch and use it for autocomplete, spell checking, word search, and IP routing. Understand when tries beat hash maps for string problems.
What Is a Trie?
A Trie (prefix tree) stores strings character by character in a tree structure. Each node represents a character, and paths from root to nodes represent prefixes. Tries enable O(L) insert, search, and prefix lookup where L is the string length — independent of how many strings are stored. This makes them ideal for autocomplete, spell checking, and prefix-based filtering.
Implementation
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:
"""Returns True if word is in the trie."""
node = self._find_node(word)
return node is not None and node.is_end
def starts_with(self, prefix: str) -> bool:
"""Returns True if any word starts with the prefix."""
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 autocomplete(self, prefix: str, limit: int = 10) -> list[str]:
"""Return all words starting with prefix."""
node = self._find_node(prefix)
if not node:
return []
results = []
def dfs(current: TrieNode, path: str):
if len(results) >= limit:
return
if current.is_end:
results.append(path)
for char, child in sorted(current.children.items()):
dfs(child, path + char)
dfs(node, prefix)
return resultsTrie vs Hash Map: Use a trie when you need prefix queries (autocomplete, prefix counting). Use a hash map when you only need exact-match lookups. Tries use more memory but enable operations hash maps can't do efficiently.
Word Search in a Grid
A classic problem: given a board of characters and a list of words, find all words that can be formed by traversing adjacent cells. Building a trie from the word list lets you prune the search early — if no word starts with the current prefix, stop exploring.
def find_words(board: list[list[str]], words: list[str]) -> list[str]:
"""Find all words from the list that exist in the board."""
trie = Trie()
for word in words:
trie.insert(word)
rows, cols = len(board), len(board[0])
result = set()
def dfs(r: int, c: int, node: TrieNode, path: str):
if node.is_end:
result.add(path)
if r < 0 or r >= rows or c < 0 or c >= cols:
return
char = board[r][c]
if char not in node.children:
return
board[r][c] = "#" # Mark visited
next_node = node.children[char]
for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
dfs(r + dr, c + dc, next_node, path + char)
board[r][c] = char # Unmark
for r in range(rows):
for c in range(cols):
dfs(r, c, trie.root, "")
return list(result)For the word search problem, optimize by removing words from the trie as you find them and pruning empty branches. This prevents redundant exploration on large word lists.