Tries (Prefix Trees)
A tree where the path from the root spells a string — giving O(L) insert and lookup that never depends on how many words you store, and making prefix search trivially fast.
What you'll learn
- Why a trie stores characters along the path, so words with a shared prefix share structure
- Why insert and lookup are O(L) — the word length — not O(dictionary size)
- How prefix search powers autocomplete almost for free
- Where tries show up — autocomplete, IP routing, spell-check, tokenizer vocabularies
Before you start
A trie — said “try”, from retrieval — is a tree where the path from the root spells out a string.
To store "cat", you walk down from the root along c, then a, then t, and mark that last spot as the end of a word. To store "car", you walk c, a, then r. Notice what just happened: the c → a part of the path was already there from "cat", so the two words share it. That shared-prefix sharing is the entire idea of a trie, and everything good about it follows from there.
One picture
Here is the trie after inserting cat, car, card, do, and dog. Green nodes mark where a complete word ends:
c → a prefix is stored once and shared by cat, car, and card. And “do” is a complete word that also sits on the way to “dog”.A node can be both an end-of-word and keep going: do ends at the o, yet dog continues through that same o. The root stands for the empty prefix; each step down adds one character.
Why the cost ignores the dictionary
Let L be the length of a word. Then insert is O(L), exact lookup is O(L), and prefix search is O(L) to reach the prefix plus O(k) to gather its k completions.
The striking thing is what is missing from those costs: the number of words in the trie. A lookup never compares your query against other words — it just follows one edge per character, steered entirely by the query itself. Whether the trie holds ten words or ten million, looking up an 8-letter word walks exactly 8 edges. A hash set also gives O(1) lookup, but it has no structure to exploit for “every word starting with ca” — it would have to scan every key. A trie answers that with a single subtree walk. The trade is memory: a trie spends nodes to buy cheap prefix queries.
Building one in Python
The cleanest version is a dictionary of dictionaries — each node maps a character to its child node, and a sentinel key marks the end of a word:
class Trie:
def __init__(self):
self.root = {}
def insert(self, word):
node = self.root
for ch in word:
node = node.setdefault(ch, {}) # walk, creating nodes as needed
node["$"] = True # mark end-of-word
def search(self, word):
node = self.root
for ch in word:
if ch not in node:
return False
node = node[ch]
return "$" in node # reached the end — but is it a word?
def startswith(self, prefix):
node = self.root
for ch in prefix:
if ch not in node:
return False
node = node[ch]
return True # reached the prefix — a subtree exists
t = Trie()
for w in ["cat", "car", "card", "do", "dog"]:
t.insert(w)
print(t.search("car")) # a stored word
print(t.search("ca")) # only a prefix, not a word
print(t.startswith("ca")) # but words do start with it
print(t.startswith("dx")) # nothing starts with "dx"
True
False
True
False
The "$" sentinel works because it can never be a real character edge, so it cleanly distinguishes “a word ends here” from “the path merely continues.” (A dedicated is_end flag on a node class does the same job, a touch more tidily.)
When a trie beats a hash map
Reach for a trie when prefix queries matter — autocomplete, type-ahead, URL routing — or when a shared prefix is worth compressing (50,000 words starting with un store that prefix once, not 50,000 times), or when you want alphabetical order for free (a depth-first walk of a trie visits words sorted). Reach for a plain hash set when you only ever need exact-match lookup and memory is tight.
Practice
Quick check
Practice this in an interview
All questionsSort each word's characters to get a canonical key, then bucket words by that key using a hash map. This turns an O(n²) brute-force comparison into a clean O(n · k log k) single pass, where k is the max word length.
At each step of a recursive walk through the input, you make a binary choice: include the current element or skip it. Recording the current path at every node of the recursion tree (not just the leaves) collects all 2^n subsets. A `start` index prevents duplicates by ensuring elements are only considered left-to-right.
Use backtracking with a running total. At each step, try adding a candidate to the current path. If the total equals the target, record the path. If it exceeds the target, prune. Passing the same start index (not i+1) back into the recursion allows unlimited reuse of the same element.
A B-tree index stores key values in a balanced tree of sorted nodes, allowing the engine to reach any value in O(log n) page reads instead of scanning every row. The optimizer skips the index when the estimated cost of random I/O exceeds a full-table scan, when a function wraps the indexed column, or when the query returns such a large fraction of rows that a sequential scan is cheaper.