PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchiTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more
A trie makes autocomplete predictable: store each string as a path of character nodes, share the nodes for common prefixes, then explore the subtree below the prefix a user typed. Insertion, exact lookup, and prefix lookup each walk at most L characters for an input of length L; producing suggestions additionally costs whatever traversal and output the suggestions require.
This guide builds a working trie in Python, including exact search, prefix checks, bounded autocomplete, deletion, normalization choices, complexity, memory trade-offs, and tests that expose the most common mistakes.
What a trie stores
The root represents the empty prefix. Every outgoing edge carries one character, and every node therefore represents the prefix reached from the root. A boolean terminal marker, commonly named is_word, records whether that prefix is a complete stored key.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →root
└─ c
└─ a
├─ r (word: true)
└─ t (word: true)
└─ d
└─ o
└─ g (word: true)
The paths for car and cat share the c and a nodes. The marker matters: if only car is stored, the node for ca still exists, but ca is not an exact key. A search must inspect the marker after consuming the final character; a prefix query only needs the path to exist.
#1 Best Overall
Choose the input contract before coding
Alphabet and case
Decide whether keys are lowercase ASCII, unrestricted Unicode, or another defined alphabet. A fixed child array can be compact and fast for a tiny known alphabet, but a mapping is safer for sparse or broad character sets. The example below uses a dictionary and applies Unicode casefold(), so Cat and cat address the same key. If your product must preserve case, replace that normalization with an identity function.
Normalization
Case folding is not the same as every form of Unicode normalization. If visually equivalent sequences must compare identically, normalize them consistently on insertion and every query, for example with a chosen Unicode normalization form before case folding. Document the policy because changing it later can make existing keys unreachable.
Empty strings and duplicates
This implementation rejects an empty key to keep the root reserved for the empty prefix. Inserting an existing key is idempotent: it sets the terminal marker again and creates no duplicate path. If your API needs an empty string as a valid key, allow the root marker and define how autocomplete of an empty prefix is capped.
Implement the trie in Python
Each node has a child mapping and one terminal flag. The public methods all normalize their input through the same helper, preventing an insertion/search mismatch.
Rank #2
class TrieNode:
__slots__ = ('children', 'is_word')
def __init__(self):
self.children = {}
self.is_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def _normalize(self, text):
return text.casefold()
def _walk(self, text):
node = self.root
for ch in text:
node = node.children.get(ch)
if node is None:
return None
return node
def insert(self, word):
word = self._normalize(word)
if not word:
raise ValueError('empty keys are not supported')
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.is_word = True
def search(self, word):
word = self._normalize(word)
node = self._walk(word)
return node is not None and node.is_word
def starts_with(self, prefix):
prefix = self._normalize(prefix)
return self._walk(prefix) is not None
def autocomplete(self, prefix, limit=10):
if limit <= 0:
return []
prefix = self._normalize(prefix)
node = self._walk(prefix)
if node is None:
return []
results = []
def visit(current, text):
if len(results) >= limit:
return
if current.is_word:
results.append(text)
for ch in sorted(current.children):
visit(current.children[ch], text + ch)
if len(results) >= limit:
return
visit(node, prefix)
return results
def delete(self, word):
word = self._normalize(word)
if not word:
return False
node = self.root
path = []
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
for parent, ch, child in reversed(path):
if child.is_word or child.children:
break
del parent.children[ch]
return True
Insertion
insert starts at the root and creates a missing child for each character. Only the final node is marked as a word. Thus inserting car after cat reuses the existing c and a nodes and adds one branch.
Exact search
search follows the complete key. A missing child immediately returns False. Reaching a node is not enough: the terminal flag must be true. With only car stored, search('ca') correctly returns false.
Prefix lookup
starts_with returns true as soon as every prefix character has a corresponding edge. It deliberately ignores is_word, because a prefix can be valid even when it is not itself stored as a key.
Autocomplete in two stages
The method first locates the node for the requested prefix. It then performs a depth-first traversal of descendants, emitting the prefix itself when that node is terminal and appending each continuation. Sorting child characters gives deterministic lexicographic output; a ranked product should replace that policy with explicit scores and a stable tie-break rule.
The limit guard stops traversal once enough results have been emitted. Without a cap, a short prefix such as a can visit a very large subtree and return an impractical response.
Deletion and pruning
Deletion first verifies that the complete key exists, clears its terminal marker, and walks the recorded path upward. A node is removed only when it has no children and is not itself another word. Shared nodes therefore survive: deleting car does not damage cat.
Run a small trace
- Create a
Trieand insertcar,cat,dog, anddoor. search('car')returns true, whilesearch('ca')returns false.starts_with('ca')returns true, andstarts_with('cz')returns false.autocomplete('ca', 5)returns['car', 'cat']under the sorted-child policy.- After
delete('car'),search('car')is false butsearch('cat')remains true and thecabranch remains intact.
Complexity: what scales with key length
Let L be the number of characters in the input key or prefix. With dictionary-like child lookup that is expected constant time, the character walk gives the following costs.
Recommended Free Tools
| Operation | Time | What the bound assumes |
|---|---|---|
| Insert | O(L) | One child lookup or creation per character. |
| Exact search | O(L) | One child lookup per character, then one terminal check. |
| Prefix lookup | O(L) | The walk stops at the first missing edge. |
| Autocomplete | O(L + V + R) | V is visited subtree work and R is the characters emitted in returned results; a limit can reduce both. |
| Delete | O(L) | The path is walked once and pruned upward at most once per character. |
The O(L) statements are not promises about every implementation. A sorted child structure, linear child list, or unusual runtime changes the cost of finding a child. Autocomplete also cannot be independent of its output: returning many long suggestions necessarily requires work to visit and construct them.
Rank #4
Trie versus simpler structures
| Structure | Exact membership | Prefix queries | Memory and update profile |
|---|---|---|---|
| Unsorted list | Linear scan through stored keys. | Usually scans every key and tests its beginning. | Small structural overhead; simple inserts, but query time grows with the collection. |
| Hash set | Expected constant-time membership under normal hash-table assumptions. | No direct prefix operation; finding matches generally requires scanning all keys or maintaining another index. | Often the simpler, leaner choice when exact membership is the only requirement. |
| Plain trie | Character walk in O(L). | Direct prefix walk followed by subtree traversal and an output policy. | More node and child-map overhead, offset by shared prefixes; supports incremental insertion and pruning. |
| Radix or compressed trie | Similar prefix-oriented behavior, with comparisons over stored substrings. | Fewer nodes on long single-child chains. | Reduces overhead but makes splitting and merging edge labels more involved. |
Choose a trie when prefix lookup or completion is a primary operation and the saved prefix work justifies its memory. Choose a hash set for a large exact-membership workload with no need to enumerate related keys. A compressed trie is worth considering when ordinary trie nodes consume too much memory because many paths have little branching.
Memory and child-representation trade-offs
A node-per-character design can use substantial memory. Every node carries object overhead, a child mapping, and references, and a broad alphabet can leave many possible characters unused. Prefix sharing helps most when keys have common beginnings; unrelated random strings share little and approach one node per character.
For a known small alphabet, an array indexed by character can avoid per-edge hash overhead and provide predictable access, but it allocates slots for absent children and requires a mapping from characters to indexes. Dictionaries handle sparse children and Unicode naturally at the cost of per-entry overhead. Measure with your actual key distribution before changing representations.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Make autocomplete useful, not merely correct
Set an explicit result policy
Define a maximum number of suggestions, whether the prefix itself appears when it is a complete key, and the ordering rule. Lexicographic order is reproducible and easy to test. Frequency, recency, language, or personalized ranking requires storing metadata and a ranking stage; the trie supplies candidates but does not decide relevance by itself.
Best Value
- Careercup, Easy To Read
- Condition : Good
- Compact for travelling
Control worst-case work
Cap results at the API boundary, reject unreasonably long inputs, and avoid traversing the entire tree when the caller needs only a handful of candidates. If ranking requires examining more candidates than are returned, document that broader candidate budget separately from the response limit.
Keep updates consistent
Apply the same normalization to bulk loading, live insertion, deletion, and queries. If keys come from multiple languages, test combining marks, case-folding edge cases, and characters outside the original alphabet. Persisting normalized keys while displaying an original spelling requires storing display text as separate payload data.
Tests and failure modes
- Prefix mistaken for a word: insert only
car; assert thatsearch('ca')is false andstarts_with('ca')is true. - Shared branch removed: insert
carandcat, deletecar, then verifycatand autocomplete forca. - Missing path: query a prefix that diverges at the first character and one that diverges near the end; both should return quickly and cleanly.
- Duplicate insertion: insert the same key repeatedly and verify that results contain it once.
- Normalization mismatch: insert mixed-case text and query a differently cased form; both should agree under the selected policy.
- Limit handling: request zero, a negative limit, one result, and a limit larger than the subtree; confirm no extra traversal results leak out.
- Ordering: insert children in different orders and verify that sorted traversal still returns the documented order.
- Deletion of a missing key: ensure the method reports false and leaves every existing key untouched.
When a trie is the right tool
Use this structure for dictionaries, command palettes, route names, usernames, or any feature whose central question is which stored keys begin with a supplied prefix. Its operations are easy to reason about because they follow characters rather than scanning unrelated records.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Do not choose a plain trie solely because autocomplete sounds fast. If the application only asks whether an entire key exists, a hash set is usually simpler and more memory-efficient. If memory is the limiting factor and keys contain long unbranched runs, a compressed trie can preserve prefix behavior with fewer nodes. In every case, specify the alphabet, normalization, result cap, and ordering policy alongside the data structure so callers know exactly what a query means.
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.

