Use a trie when your application must answer prefix questions such as “which words start with ca?” A trie stores each string as a path of characters, sharing the nodes for common prefixes. Once the path for a prefix is found, autocomplete explores only that prefix’s subtree instead of comparing the prefix with every stored word.
This guide builds a working trie from scratch, including insertion, exact search, prefix checks, bounded autocomplete, and safe deletion. It also explains the memory cost, the assumptions behind the usual O(L) claims, and when a hash set or compressed trie is a better choice.
As an Amazon Associate I earn from qualifying purchases.
What a trie stores
A trie (pronounced “try,” from retrieval) has a root node representing the empty string. Each outgoing edge carries one character, and every node therefore represents the prefix reached from the root.
root
└─ c
└─ a
├─ t (is_word = true) "cat"
└─ r
└─ (is_word = true) "car"
The is_word flag is essential. In this example, ca is a valid path but is not necessarily a stored word; cat and car are complete keys because their final nodes are marked. Without that terminal marker, exact search could incorrectly report every prefix of a longer word as present.
#1 Best Overall
Shared prefixes are the reason a trie helps with autocomplete. The strings cat, car, and cart reuse the same c → a → r nodes. The trade-off is one object per character (in a basic implementation), plus a child mapping on each object.
Decide the input model before writing code
Character set
A fixed lowercase-English dictionary can use the alphabet a through z. A general text field may contain uppercase letters, punctuation, emoji, or characters from several scripts. A dictionary-based child map works for all of these, provided the keys are individual characters in your language runtime.
Case and normalization
Choose one policy and apply it to both inserted keys and queries. Common policies are case-sensitive storage, lowercasing for case-insensitive search, or Unicode normalization followed by case folding. Normalization can change how visually equivalent sequences compare, so document it as part of the API rather than silently mixing policies.
Free tools Windows power users keep installed
One-click scans. No signup required.
Child-container choice
| Representation | Strength | Cost or limitation |
|---|---|---|
| Map/dictionary | Supports sparse children and broad character sets | Each node carries map overhead; lookup is expected O(1) in typical hash-map runtimes |
| Fixed array | Fast indexing for a small, known alphabet | Allocates slots even for absent characters; awkward for Unicode |
| Sorted child list | Can reduce map overhead in tiny sparse nodes | Child lookup may require a scan or binary search |
The implementation below uses a dictionary because it is easy to understand and does not assume an alphabet. Its complexity statements rely on the runtime offering expected constant-time dictionary access.
Rank #2
- color: White
- INTRODUCTION TO ALGORITHMS, FOURTH EDITION
Build the trie in Python
The root is created once and represents the empty prefix. Every other node stores its outgoing edges in children and records whether a complete key ends there.
class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
"""Store word; inserting an existing word is harmless."""
node = self.root
for ch in word:
node = node.children.setdefault(ch, TrieNode())
node.is_word = True
def search(self, word):
"""Return True only when word was inserted as a complete key."""
node = self._node_for(word)
return node is not None and node.is_word
def starts_with(self, prefix):
"""Return True when the character path for prefix exists."""
return self._node_for(prefix) is not None
def _node_for(self, text):
node = self.root
for ch in text:
node = node.children.get(ch)
if node is None:
return None
return node
def autocomplete(self, prefix, limit=None):
"""Return completions in depth-first, child-insertion order."""
start = self._node_for(prefix)
if start is None:
return []
results = []
def visit(node, word):
if limit is not None and len(results) >= limit:
return
if node.is_word:
results.append(word)
for ch, child in node.children.items():
visit(child, word + ch)
if limit is not None and len(results) >= limit:
return
visit(start, prefix)
return results
def delete(self, word):
"""Remove word and prune nodes no longer needed by another key."""
path = []
node = self.root
for ch in word:
child = node.children.get(ch)
if child is None:
return False
path.append((node, ch, child))
node = child
if not node.is_word:
return False
node.is_word = False
# Work upward, removing only leaf nodes that are not word endings.
for parent, ch, child in reversed(path):
if child.is_word or child.children:
break
del parent.children[ch]
return True
setdefault creates a child only when the character is missing. The private _node_for helper is shared by exact search, prefix lookup, and completion, which keeps their path-walking behavior consistent.
How each operation behaves
Insertion
To insert car, begin at the root, create or follow the c child, then a, then r. Mark the final node. If cat is inserted afterward, only the new t node is needed because c and a already exist.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Exact search
search("car") follows all three characters and then checks is_word. Searching for ca returns false when only car is stored, even though the path exists. Searching for a missing edge returns false immediately.
Rank #3
Prefix lookup
starts_with("ca") succeeds as soon as the c → a path is found. It does not inspect the terminal flag because a prefix need not be a complete key. An empty prefix follows zero edges and therefore matches the root; decide whether your product should treat that as valid before exposing it in a user interface.
Autocomplete
Completion has two distinct stages:
- Walk the requested prefix to locate its node. If any character is absent, return an empty list.
- Traverse descendants, carrying the full text accumulated so far and emitting every node whose
is_wordflag is true.
The method accepts an optional limit. A limit prevents a broad prefix such as an empty string or a from materializing an unbounded result set. The sample returns depth-first results in dictionary insertion order; that is an implementation detail, not a relevance ranking.
Ordering, ranking, and result limits
Users usually expect suggestions in a deliberate order. Alphabetical order requires sorting children or sorting the completed result list. Frequency, recency, locale, spelling quality, and personalization require metadata beyond a plain terminal flag.
Recommended Free Tools
Keep ordering separate from prefix discovery. A practical design is to store a score or popularity value at terminal nodes, collect at most a bounded number of candidates, and rank them according to a documented policy. If you need globally best results under a prefix, a simple depth-first walk may visit many low-ranked descendants before finding the best matches; maintaining ranked child lists or using a specialized top-k structure can reduce that work at the cost of more update complexity.
Deleting a key without breaking shared prefixes
Deletion has two cases. If car and cart are stored, deleting car must clear the terminal flag at r but retain the path because cart still uses it. If cart is then deleted, the now-unneeded t node can be removed, followed by any parent nodes that have no children and are not terminal nodes.
The implementation records the path while searching, clears the final marker, and prunes upward until it reaches a shared branch or another stored word. It returns False when the key was absent or was only a prefix, so callers can distinguish a real deletion from a no-op.
Complexity: what O(L) really means
Let L be the length of the inserted word or queried prefix. With expected constant-time map operations, insertion, exact search, and prefix lookup each walk at most L edges.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →| Operation | Time | Why |
|---|---|---|
| Insert | O(L) |
One child lookup or creation per character |
| Exact search | O(L) |
One path walk plus a terminal-flag check |
| Prefix lookup | O(L) |
Stops after the prefix path is consumed |
| Autocomplete | O(L + V + R) |
L to find the prefix, then V traversal work and R emitted results |
| Delete | O(L) |
One downward walk and at most L upward pruning checks |
For autocomplete, V is the amount of the matching subtree visited; it can be much larger than the number of returned results when ranking, filtering, or a late limit is used. The cost also includes constructing the output strings. If child containers are arrays, lookup is direct for the supported alphabet. If they are sorted lists or another structure, include that child-lookup cost instead of assuming constant time.
Best Value
- Binding: paperback
- Language: english
- It ensures you get the best usage for a longer period
Trie versus other storage choices
| Need | Basic trie | Hash set | Sorted list | Compressed (radix) trie |
|---|---|---|---|---|
| Exact membership | Fast by key length, but node overhead may be unnecessary | Usually the simplest and most memory-efficient choice | Typically requires binary search | Fast, with more complex node handling |
| Prefix existence | Natural path walk | Requires scanning or an additional index | Can use a range of neighboring entries | Natural path walk over longer edge labels |
| Autocomplete | Descend directly from the prefix node | No direct operation | Scan the matching sorted range | Descend with fewer nodes |
| Memory behavior | One node per character plus child containers; sharing helps when prefixes repeat | Stores complete keys and hash-table overhead | Stores complete keys and list overhead | Collapses single-child chains to reduce node count |
| Updates | Insertion and pruning are straightforward | Very straightforward for membership | Insertion can shift elements or require a tree | Splitting and merging edge labels add implementation work |
If your only question is “is this exact identifier present?”, a hash set is generally simpler. Choose a trie when prefix queries are central and the memory budget can accommodate its per-node overhead. A radix or compressed trie is worth considering when many branches contain long runs of single children, because it stores those runs as one multi-character edge.
Memory and production considerations
- Measure node count. A dictionary child map can dominate memory when most nodes have only one child. Prefix sharing reduces stored characters but does not remove object and map overhead.
- Bound output. Require a maximum number of suggestions and, where appropriate, a maximum prefix length. This protects latency and response size for broad queries.
- Keep normalization consistent. Applying lowercasing during insertion but not during lookup creates apparently missing words; applying it only at lookup can create collisions that were not intended.
- Separate text from metadata. Popularity, locale, display labels, and permissions belong on terminal records or an associated data store rather than being encoded in the character path.
- Consider concurrency. Read-heavy services can build an immutable trie and replace it atomically when a new snapshot is ready. In-place updates need synchronization appropriate to the language runtime.
- Protect sensitive data. A trie can expose every stored key through broad prefix queries. Apply authorization and filtering before returning suggestions, especially for private names or identifiers.
Testing checklist
Use a small fixture that includes shared prefixes, complete words that are also prefixes, and a missing branch:
words = ["car", "cart", "cat", "dog"]
trie = Trie()
for word in words:
trie.insert(word)
assert trie.search("car")
assert trie.search("cart")
assert not trie.search("ca")
assert trie.starts_with("ca")
assert not trie.starts_with("cow")
assert set(trie.autocomplete("car")) == {"car", "cart"}
assert trie.autocomplete("zz") == []
assert trie.delete("car")
assert not trie.search("car")
assert trie.search("cart")
assert not trie.delete("car")
Add tests for the empty string policy, repeated insertion, case and Unicode normalization, a limit of zero, a limit larger than the available results, deleting a missing key, and deleting a word that is a prefix of another word. If you promise alphabetical or ranked output, assert that ordering explicitly rather than relying on dictionary iteration order.
Choosing the right design
Build the basic map-backed trie when you need predictable prefix operations, a transparent implementation, and moderate data volume. Use a hash set for exact membership without completion. Use a sorted representation when you already maintain ordered records and can tolerate scanning a prefix range. Move to a compressed trie when profiling shows that object and map overhead—not character-walk time—is the limiting factor.
The core idea remains small: walk characters to find a prefix, mark complete keys separately, and traverse only the descendants needed for the requested suggestions. The quality of a real autocomplete feature then depends on the policies around that structure—normalization, limits, ordering, ranking, authorization, and update strategy.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

