Tries (Prefix Trees)
Table of Contents
Introduction
A Trie (pronounced “try”) is a tree-like data structure used to store and search strings efficiently. Each node represents a single character, and paths from root to node spell out prefixes. It’s the data structure behind autocomplete, spell checkers, and IP routing tables.
The name comes from “retrieval” — tries are optimized for looking up strings by prefix. Unlike a hash table which gives you O(1) lookup for exact keys, a trie gives you O(m) lookup (where m is the key length) plus the ability to find all keys that share a prefix.
You should be familiar with Binary Trees and Big-O Notation before reading this.
How a Trie works
Insert: "cat", "car", "card", "dog", "do"
(root)
/ \
c d
| |
a o *
| |
t * g *
|
r *
|
d *
* = end of word
Each path from root to a * node spells a complete word. The prefix “ca” is shared by “cat”, “car”, and “card”.
Implementation
Autocomplete with a Trie
The killer feature — find all words that start with a given prefix:
class Trie {
// ... previous methods
autocomplete(prefix) {
const node = this._findNode(prefix);
if (!node) return [];
const results = [];
this._collect(node, prefix, results);
return results;
}
_collect(node, prefix, results) {
if (node.isEnd) results.push(prefix);
for (const [char, child] of Object.entries(node.children)) {
this._collect(child, prefix + char, results);
}
}
}
const trie = new Trie();
['cat', 'car', 'card', 'care', 'cargo', 'dog'].forEach(w => trie.insert(w));
console.log(trie.autocomplete('car')); // ['car', 'card', 'care', 'cargo']
console.log(trie.autocomplete('do')); // ['dog']
console.log(trie.autocomplete('z')); // []
Delete from a Trie
class Trie {
// ... previous methods
delete(word) {
this._deleteHelper(this.root, word, 0);
}
_deleteHelper(node, word, depth) {
if (!node) return false;
if (depth === word.length) {
if (!node.isEnd) return false;
node.isEnd = false;
return Object.keys(node.children).length === 0;
}
const char = word[depth];
const shouldDelete = this._deleteHelper(node.children[char], word, depth + 1);
if (shouldDelete) {
delete node.children[char];
return !node.isEnd && Object.keys(node.children).length === 0;
}
return false;
}
}
Delete is trickier — you need to remove nodes bottom-up, but only if they’re not part of another word’s path.
Complexity analysis
| Operation | Time | Space |
|---|---|---|
| Insert | O(m) | O(m) per word |
| Search | O(m) | O(1) |
| Prefix search | O(m) | O(1) |
| Autocomplete | O(m + k) | O(k) |
| Delete | O(m) | O(1) |
Where m = length of the string, k = number of results.
Total space: O(n × m) in the worst case where n is the number of words and m is the average length. In practice, shared prefixes reduce this significantly.
Trie vs Hash Table
| Trie | Hash Table | |
|---|---|---|
| Exact lookup | O(m) | O(m) average |
| Prefix search | O(m) ✅ | Not supported ❌ |
| Ordered iteration | Yes (alphabetical) | No |
| Space | Higher (node overhead) | Lower |
| Autocomplete | Natural | Requires scanning all keys |
Use a trie when you need prefix operations. Use a hash table when you only need exact key lookup.
Practice problems
- Implement Trie (LeetCode 208)
- Word Search II (LeetCode 212)
- Design Add and Search Words (LeetCode 211)
- Replace Words (LeetCode 648)
- Longest Word in Dictionary (LeetCode 720)