Trie (Prefix Tree)
A trie stores words as a tree of letters: one node per character, one path per word. Words that share a start also share nodes - "car" and "card" overlap on three of them. Looking up a word costs one step per letter, no matter how many words the trie holds. That is why autocomplete can search a million words while you are still typing.
Watch the trie grow
Press play - or drag the timeline and step through it yourself.Start with an empty trie: just a root spelling "". We insert cat, car, card, dog and do - then search for three words.
Complexity
The contacts app on your phone
Open your contacts and type 'j'. The list instantly shrinks to Jack, Jamie and Jo. Type 'a' and Jo disappears. Each key press is one step down a tree of letters: you move from the 'j' node to the 'ja' node, and every name hanging below that node is the suggestion list. The app never re-reads all thousand contacts on a key press - it just takes one more step down. A trie is that tree.
How it works, step by step
Every node holds a map from letters to child nodes, plus one boolean flag. The root is an empty node: it spells the empty prefix.
Insert: walk down one letter at a time. If the child for that letter exists, step into it. If not, create it. This is why shared prefixes are stored only once.
After the last letter of an insert, set the word-end flag on the node you stand on. That flag is what makes "do" a real word instead of just the start of "dog".
Search: walk the same letters, but never create. A missing letter means the word cannot be there - stop at once.
Reached the last letter? Now check the flag. Letters alone only prove a prefix exists - that is exactly the question `startsWith` answers.
Every operation touches at most m nodes, where m is the length of the word. Ten words stored, or ten million - the walk is the same.
The code, in JavaScript
The LeetCode 208 trio with a plain object as the children map. One shared walk helper - the flag check is the only thing separating search from startsWith.
function createTrie() {
// A node is just: children map + one flag. That flag IS the design.
return { children: {}, isWord: false };
}
function insert(root, word) {
let node = root;
for (const ch of word) {
// No child for this letter yet? Create it. Otherwise reuse it -
// this line is why "car" and "card" share three nodes.
node.children[ch] ??= createTrie();
node = node.children[ch];
}
node.isWord = true; // flag the END node - not the letters
}
function walk(root, letters) {
let node = root;
for (const ch of letters) {
node = node.children[ch];
if (node === undefined) return null; // path breaks - stop early
}
return node;
}
function search(root, word) {
const node = walk(root, word);
return node !== null && node.isWord; // letters exist AND flag is set
}
function startsWith(root, prefix) {
return walk(root, prefix) !== null; // letters exist - no flag needed
}
const trie = createTrie();
["cat", "car", "card", "dog", "do"].forEach((w) => insert(trie, w));
search(trie, "car"); // → true (flag set on the 'r' node)
search(trie, "ca"); // → false (nodes exist, flag missing)
startsWith(trie, "ca"); // → true (a prefix is enough here)
search(trie, "cow"); // → false (no 'o' under 'c')Dry run: 5 inserts, then 3 searches
The same steps the visualizer plays, written as a table. Generated by running the real algorithm - not written by hand.
| Step | Word | What happened |
|---|---|---|
| 1 | 1 | No 'c' under the root - create it. |
| 2 | 1 | No 'a' under 'c' - create it. |
| 3 | 1 | No 't' under 'a' - create it. |
| 4 | 1 | Mark 't' as a word end - "cat" is now stored. |
| 5 | 2 | No 'r' under 'a' - create it. |
| 6 | 2 | Mark 'r' as a word end - "car" is now stored. It shares two nodes with "cat". |
| 7 | 3 | No 'd' under 'r' - create it. |
| 8 | 3 | Mark 'd' as a word end - "card" is now stored. "car" and "card" share three letter nodes. |
| 9 | 4 | No 'd' under the root - create it. |
| 10 | 4 | No 'o' under 'd' - create it. |
| 11 | 4 | No 'g' under 'o' - create it. |
| 12 | 4 | Mark 'g' as a word end - "dog" is now stored. |
| 13 | 5 | 'd' is already there - walk in. |
| 14 | 5 | 'o' is already there - walk in. |
| 15 | 5 | Mark 'o' as a word end - "do" is now stored. Nothing was built - only the flag changed. Without it, "do" would be invisible inside "dog". |
| 16 | 6 | 'c' found under the root - go deeper. |
| 17 | 6 | 'a' found under 'c' - go deeper. |
| 18 | 6 | 'r' found - that was the last letter. Now check the flag on this node. |
| 19 | 6 | 'r' has the word-end flag - "car" is stored. Found in 3 steps; the number of words never mattered. |
| 20 | 7 | 'c' found under the root - go deeper. |
| 21 | 7 | 'a' found - that was the last letter. Now check the flag on this node. |
| 22 | 7 | 'a' has no word-end flag - "ca" is only a prefix here, not a stored word. |
| 23 | 8 | 'c' found under the root - go deeper. |
| 24 | 8 | No 'o' under 'c' - stop, the word cannot exist. No word list was ever scanned. |
Good choice when…
- Autocomplete and search-as-you-type: stand on the prefix node, and everything below it is the suggestion list.
- Prefix questions in general: how many words start with "ca", the longest common prefix of a set, does any stored word start with this.
- Word games and spell checkers (Boggle, Scrabble racks): the trie cuts off a whole branch the moment a prefix stops matching any word.
- Longest-prefix matching, like IP routing tables: the deepest flagged node along your path is the best match.
Bad choice when…
- You only ever need exact lookups. A plain `Map` or `Set` is one hash away, simpler, and usually faster.
- Memory is tight and your words share little: every node is an object, and a word with no shared prefix costs one node per letter.
- Your keys are not sequences (numbers, ids, objects). With no prefix structure, a trie has nothing to exploit.
Common mistakes
- The word-end flag is the whole design. Without it, "do" and "dog" are indistinguishable - and a search that forgets the flag check is just `startsWith` with a misleading name.
- Fixed 26-slot child arrays explode memory: every node pays for 26 pointers even when it has one child. Use an object or `Map` unless the alphabet is truly small and dense.
- A trie beats a hash map only for prefix questions. For exact lookup a hash touches the word once; the trie chases one pointer per letter and loses on cache misses.
- Deleting a word must not delete shared nodes: removing "car" has to keep 'c'-'a'-'r' alive while "card" still needs them. Refcount each node (the Count + delete tab) or prune only child-less, unflagged nodes.
Trie (Prefix Tree) vs. its closest relatives
| Algorithm | Best | Average | Worst | Space | Stable |
|---|---|---|---|---|---|
| Trie (Prefix Tree)this page | O(m) | O(m) | O(m) | O(n·m) | - |
| Binary Search Tree | O(log n) | O(log n) | O(n) | O(n) | - |
| KMP Pattern Matching | O(n + m) | O(n + m) | O(n + m) | O(m) | - |
| Tree Traversals | O(n) | O(n) | O(n) | O(h) | - |