Implement Trie (Prefix Tree)
Interview Prep
The problem
Build a trie (prefix tree) supporting three operations:
insert(word), search(word) (exact match),
and startsWith(prefix). All three should be
O(L) where L is the string length, with
no dependence on the number of stored words.
Pattern: per-character branching with terminal flag
Each node is a small map from a character to a child node, plus a
boolean marking whether some inserted word ends exactly here.
Walking the tree consumes one character per step; the difference
between search and startsWith is just the
terminal-flag check at the end. Two functions, one walk.
The terminal flag is what lets the trie distinguish stored words
from mere prefixes. Without it, after inserting "apple"
the trie wouldn't be able to say whether "app" is a
stored word or just a path on the way somewhere.
Solution
class Trie:
"""Prefix tree over arbitrary alphabet. Each node holds a children dict + a terminal flag."""
def __init__(self):
self.children: dict[str, 'Trie'] = {}
self.is_word: bool = False
def insert(self, word: str) -> None:
node = self
for ch in word:
node = node.children.setdefault(ch, Trie())
node.is_word = True
def search(self, word: str) -> bool:
node = self._walk(word)
return node is not None and node.is_word
def startsWith(self, prefix: str) -> bool:
return self._walk(prefix) is not None
def _walk(self, s: str) -> 'Trie | None':
node = self
for ch in s:
node = node.children.get(ch)
if node is None:
return None
return node Trace
Insert "app", "apple", "ape", "bat":
(root)
├── 'a' -> A
│ ├── 'p' -> AP
│ │ ├── 'p' -> APP (is_word)
│ │ │ └── 'l' -> APPL
│ │ │ └── 'e' -> APPLE (is_word)
│ │ └── 'e' -> APE (is_word)
└── 'b' -> B
└── 'a' -> BA
└── 't' -> BAT (is_word)
search("app") : walk a -> p -> p, node.is_word = True -> True
search("appl") : walk a -> p -> p -> l, node.is_word = False -> False
search("apple") : walk a -> p -> p -> l -> e, True -> True
startsWith("ap"): walk a -> p succeeds -> True
startsWith("ax"): walk a -> x, child missing -> False Variant: fixed-size array for ASCII
If the alphabet is small and fixed (e.g., 26 lowercase letters), replace the dict with a 26-slot array. Slightly faster lookups, slightly more memory for sparse nodes. Choose based on alphabet size and density.
class TrieArray:
"""Lower-cased ASCII only — slightly faster, slightly less memory."""
__slots__ = ('next', 'is_word')
def __init__(self):
self.next: list['TrieArray | None'] = [None] * 26
self.is_word: bool = False
def insert(self, word):
node = self
for ch in word:
i = ord(ch) - ord('a')
if node.next[i] is None:
node.next[i] = TrieArray()
node = node.next[i]
node.is_word = True Complexity
- Insert / search / startsWith:
O(L)time, whereL= key length. - Space:
O(total characters across all stored words)in the worst case.
Variations worth knowing
- Word Search II: given a board and a list of words, find all words present. Build a trie from the dictionary, then DFS the board guided by the trie — orders of magnitude faster than running Word Search per word.
- Autocomplete / typeahead: walk the trie to the
prefix node, then DFS to collect all descendants with
is_word = True. Top-k extension: store the most popular completion at each node. - Replace Words: for each input word, the trie
gives the shortest stored prefix in
O(L). - Maximum XOR of two numbers: binary trie over the
bits of each integer; greedy walk on the opposite bit at every
step.
O(32n)total. - Compressed tries (Patricia / radix tree): collapse chains of single-child nodes into single edges labelled with the substring. Major memory win for long, sparse keys. Foundational for many database indexes and IP routing tables.