Tries (Prefix Trees)
Store strings character by character in a tree so prefix lookups cost O(length of word), not O(number of words).
- O(L)Per insert, search or prefix check
- O(S)Build time and nodes; S is total characters
- 26 slotsPer node with an array of children
Show what to say
“For many prefix questions, a dictionary matched against a sentence or a grid, or wildcards, I build a trie. Words share nodes along their prefixes, so insert, search and startsWith cost O(L) for a word of length L however many words are stored, and a search can stop the moment it leaves the trie.”
Trade-offs
- Trie vs hash set: a Set is simpler, smaller and also O(L) for exact words; a trie wins on prefixes, wildcards and early stopping.
- Map vs array of 26: the array is fastest for a small known alphabet but always spends 26 slots; a Map stores only existing children.
- Top-k per node vs DFS below the prefix: O(L) queries for more memory and slower inserts, against a DFS per query.
Bring it up before they ask
search must check the end flag, not just that the path exists (car is a path inside cart), and a grid search must mark cells visited and restore them when backtracking.
A trie stores words letter by letter along paths from the root, so words with the same start share nodes. Insert, lookup and prefix checks cost O(L) for a word of length L, however many words are stored.
When to reach for it
Tap the signals you can see in the problem.
RememberprefixdictionaryWildcards
- Basic trie
What you do
children+ an end flag; insert, search, startsWith - Prefix walk, early stop
What you dostop at the first end flag or missing edge
- Wildcard DFS
What you doa
.tries every child - Trie-guided backtracking
What you doa grid DFS moves a trie pointer in step
The core idea: a prefix is a path
Think of the thumb tabs on a paper dictionary: every word starting with "ca" sits behind the same two tabs.
Read more · 78 words
A trie works the same way. Each node stands for a prefix, the letters on the path from the root. Inserting "car" makes (or reuses) root → c → a → r, and marks the last node as a word end.
A lookup follows one edge per letter: O(L) whether the trie holds ten words or a million. A hash set is also O(L) per word, but can't answer "does any word start with ca?" without scanning them all.
Insert: shared prefixes share nodes
- nodes
- 1
An empty trie is just a root node. Each edge below it will stand for one character, and a badge will mark a node where a whole word ends.
insert(word) {
let node = this.root;
for (const ch of word) {
if (!node.children.has(ch)) {
node.children.set(ch, new TrieNode());
}
node = node.children.get(ch);
}
node.isEnd = true;
}All steps as text
- An empty trie is just a root node. Each edge below it will stand for one character, and a badge will mark a node where a whole word ends.
- Insert "car": start at the root and read it one character at a time.
- No edge for 'c' yet, so create a node for "c" and step into it.
- No edge for 'a' yet, so create a node for "ca" and step into it.
- No edge for 'r' yet, so create a node for "car" and step into it.
- Mark the last node: a word ends at "car". Without this flag, "car" would only be a prefix of other words.
- Insert "cat": start at the root and read it one character at a time.
- "c" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
- "ca" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
- No edge for 't' yet, so create a node for "cat" and step into it.
- Mark the last node: a word ends at "cat". Without this flag, "cat" would only be a prefix of other words.
- Insert "cart": start at the root and read it one character at a time.
- "c" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
- "ca" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
- "car" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
- No edge for 't' yet, so create a node for "cart" and step into it.
- Mark the last node: a word ends at "cart". Without this flag, "cart" would only be a prefix of other words.
- Insert "dog": start at the root and read it one character at a time.
- No edge for 'd' yet, so create a node for "d" and step into it.
- No edge for 'o' yet, so create a node for "do" and step into it.
- No edge for 'g' yet, so create a node for "dog" and step into it.
- Mark the last node: a word ends at "dog". Without this flag, "dog" would only be a prefix of other words.
- Insert "do": start at the root and read it one character at a time.
- "d" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
- "do" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
- Mark the last node: a word ends at "do". Without this flag, "do" would only be a prefix of other words.
- 5 words with 15 characters in total needed only 8 nodes below the root: shared prefixes are stored once. Each insert cost O(length of the word).
Play with it
Watch new words reuse shared prefixes. Then type a prefix to light its path, or try a wildcard such as c.t.
Read more · 68 words
Step by step
Pick a node shape
Children by letter, plus an end flag.
Insert every word
Create missing children, mark the end.
Decide what a query needs at the end
A word needs the flag; a prefix needs only the path.
Walk the query and stop early
Stop when an edge is missing.
Let the trie steer other searches
Carry a trie node along; drop dead prefixes.
The Trie class
Template 1: the Trie class
find(prefix) walks the path and returns the node it reaches, or null.
Read more · 32 words
search needs that node and its end flag; startsWith only needs the node.
This class is Implement a Trie (Prefix Tree)Medium. For Longest Common PrefixEasy, walk down while the node has one child and isn't a word end.
// A trie node: one child per next character, plus a flag that says "a word ends here".
class TrieNode {
constructor() {
this.children = new Map(); // character -> TrieNode
this.isEnd = false;
}
}
export class Trie {
constructor() {
this.root = new TrieNode();
}
insert(word) {
let node = this.root;
for (const ch of word) {
if (!node.children.has(ch)) node.children.set(ch, new TrieNode());
node = node.children.get(ch);
}
node.isEnd = true; // the path alone doesn't make a word: mark where this one ends
}
// The node reached by following prefix from the root, or null if the path breaks off.
find(prefix) {
let node = this.root;
for (const ch of prefix) {
node = node.children.get(ch);
if (!node) return null;
}
return node;
}
search(word) {
const node = this.find(word);
return node !== null && node.isEnd; // the path must exist AND end a word
}
startsWith(prefix) {
return this.find(prefix) !== null; // any path at all is enough
}
// Autocomplete: every stored word that starts with prefix, found by a DFS below the prefix's node.
wordsWithPrefix(prefix) {
const out = [];
const dfs = (node, path) => {
if (node.isEnd) out.push(path);
for (const [ch, child] of node.children) dfs(child, path + ch);
};
const start = this.find(prefix);
if (start) dfs(start, prefix);
return out;
}
}search vs startsWith: the same walk, a different last check
The trie holds car, cat, cart, dog. Badged nodes end a word. Every query walks down from the root, one character per step.
find(prefix) {
let node = this.root;
for (const ch of prefix) {
node = node.children.get(ch);
if (!node) return null;
}
return node;
}
search(word) { const n = this.find(word); return n !== null && n.isEnd; }
startsWith(prefix) { return this.find(prefix) !== null; }All steps as text
- The trie holds car, cat, cart, dog. Badged nodes end a word. Every query walks down from the root, one character per step.
- search("cart"): start at the root.
- Follow 'c' to "c".
- Follow 'a' to "ca".
- Follow 'r' to "car".
- Follow 't' to "cart".
- The whole query was read and "cart" is marked as the end of a word: search is true.
- search("ca"): start at the root.
- Follow 'c' to "c".
- Follow 'a' to "ca".
- The path "ca" exists, but no word ends there (it is only a prefix of car, cat, cart): search is false.
- startsWith("ca"): start at the root.
- Follow 'c' to "c".
- Follow 'a' to "ca".
- The whole prefix was read without falling off the trie, so at least one word starts with "ca": startsWith is true. It doesn't care whether a word ends here.
- search("cow"): start at the root.
- Follow 'c' to "c".
- "c" has no 'o' child. No stored word starts with "co", so the walk stops after 2 steps, however many words the trie holds.
- find returned null, so search("cow") is false.
- Each query cost at most one step per character of the query: O(L), independent of how many words are stored. search("cart") = true; search("ca") = false; startsWith("ca") = true; search("cow") = false.
Choosing the node shape
Read more · 74 words
| Children stored as | Find a child | Memory per node | Watch out for |
|---|---|---|---|
Map | children.get(ch), hashing | only existing children, any character | get returns undefined, not null |
| plain object | children[ch] | only existing children, any character | use Object.create(null) against inherited keys |
| array of 26 | next[code - 97], no hashing | always 26 slots, a to z only | other characters index out of range |
An array is fastest; a Map is the safe default.
// The same trie with a fixed array of 26 children per node, for lowercase 'a' to 'z' only.
// No hashing, so it is fast and predictable; but every node pays for 26 slots even if it uses one.
const CODE_A = 'a'.charCodeAt(0);
const arrayNode = () => ({ next: new Array(26).fill(null), isEnd: false });
export class ArrayTrie {
constructor() {
this.root = arrayNode();
}
insert(word) {
let node = this.root;
for (let i = 0; i < word.length; i++) {
const c = word.charCodeAt(i) - CODE_A; // 'a' -> 0, 'b' -> 1, … 'z' -> 25
node.next[c] ??= arrayNode();
node = node.next[c];
}
node.isEnd = true;
}
search(word) {
let node = this.root;
for (let i = 0; i < word.length && node !== null; i++) node = node.next[word.charCodeAt(i) - CODE_A];
return node !== null && node.isEnd;
}
}Wildcards and pruning
Template 2: wildcard search with DFS
A . has several edges to follow, so the walk becomes a DFS: a letter follows one edge, a dot tries every child.
This is Design Add and Search WordsMedium.
Wildcard search: a dot tries every child
- pattern
- "..t"
- i
- 0
Search "..t" in a trie of bad, bed, cat, cod. A dot can be any letter, so the search may have to try several branches; a letter allows only one.
search(pattern) {
const dfs = (node, i) => {
if (i === pattern.length) return node.isEnd;
const ch = pattern[i];
if (ch !== ".") {
const child = node.children.get(ch);
return child !== undefined && dfs(child, i + 1);
}
for (const child of node.children.values()) {
if (dfs(child, i + 1)) return true;
}
return false;
};
return dfs(this.root, 0);
}All steps as text
- Search "..t" in a trie of bad, bed, cat, cod. A dot can be any letter, so the search may have to try several branches; a letter allows only one.
- pattern[0] is a dot: try every child of the root in turn ('b', 'c') and stop at the first that leads to a match.
- pattern[1] is a dot: try every child of "b" in turn ('a', 'e') and stop at the first that leads to a match.
- pattern[2] is 't', but "ba" has no 't' child. This branch is dead: go back and try the next option.
- pattern[2] is 't', but "be" has no 't' child. This branch is dead: go back and try the next option.
- Every child of "b" failed, so this call returns false.
- pattern[1] is a dot: try every child of "c" in turn ('a', 'o') and stop at the first that leads to a match.
- pattern[2] is the letter 't': there is only one edge to follow, to "cat".
- The whole pattern is used up at "cat", and a word ends here. Match found.
- search("..t") is true. The dead branches cost a few steps each; branches that don't match the letters are never entered.
// Wildcard search on a trie (built with the Trie template): '.' matches any one character.
// A letter follows a single edge; a dot branches into every child. A branch that breaks off dies at once.
export function matchesPattern(trie, pattern) {
const dfs = (node, i) => {
if (i === pattern.length) return node.isEnd; // used up the pattern: is this a whole word?
const ch = pattern[i];
if (ch !== '.') {
const child = node.children.get(ch);
return child !== undefined && dfs(child, i + 1);
}
for (const child of node.children.values()) {
if (dfs(child, i + 1)) return true; // stop at the first branch that matches
}
return false;
};
return dfs(trie.root, 0);
}Template 3: prune a search with the trie
The big win: let another search (a text scan, a grid walk) move a trie pointer. At each step:
Read more · 34 words
- no child for the next letter: stop this branch;
- the node ends a word: record it, and keep going.
Replace WordsMedium stops at the first end flag. Word Search IIHard runs a backtracking DFS from every cell.
// Pruned search: from each start position, walk the trie while reading the text, and stop the moment no stored
// word continues that way. Finds every dictionary word that appears in text, as [start, word] pairs.
export function findAllMatches(text, trie) {
const found = [];
for (let start = 0; start < text.length; start++) {
let node = trie.root;
for (let end = start; end < text.length; end++) {
node = node.children.get(text[end]);
if (!node) break; // dead end: no word has this prefix, so stop early
if (node.isEnd) found.push([start, text.slice(start, end + 1)]);
}
}
return found;
}Try it
// Add words and queries, then press Run. Trie, matchesPattern and findAllMatches are the templates above.
const trie = new Trie();
for (const word of ['car', 'cart', 'cat', 'care', 'dog']) trie.insert(word);
console.log(trie.search('car'), trie.search('ca'), trie.startsWith('ca')); // true false true
console.log(trie.wordsWithPrefix('car')); // ['car', 'cart', 'care']
console.log(matchesPattern(trie, 'c.t'), matchesPattern(trie, '.o.')); // true true
console.log(matchesPattern(trie, 'ca..'), matchesPattern(trie, 'd..s')); // true false
console.log(findAllMatches('scatdogcare', trie)); // [[1, 'cat'], [4, 'dog'], [7, 'car'], [7, 'care']]Complexity and JavaScript
Complexity
Read more · 128 words
| Operation | Time | Extra space |
|---|---|---|
| Insert one word | O(L) | up to L new nodes |
search, startsWith | O(L) | O(1) |
| Build from all words | O(total letters) | O(total letters) nodes (each with 26 slots for arrays) |
| Autocomplete below a prefix | O(L + size of that subtree) | O(depth) for the DFS |
| Wildcard search | O(L) without dots; with many, every node to depth L | O(L) recursion |
| Text scan from every position | O(n · longest word) at most, far less with pruning | O(1) besides the trie |
JavaScript you'll use
JavaScript refresher: Classes for data structures Map and Set Strings and charactersclass TrieNode {
constructor() { this.children = new Map(); this.isEnd = false; }
}
let child = node.children.get(ch); // undefined when the edge is missing
for (const ch of word) { /* … */ } // iterate characters
const index = word.charCodeAt(i) - 97; // 'a' → 0 … 'z' → 25 for array children
for (const [ch, next] of node.children) {} // DFS over the children that exist
const kids = Object.create(null); // an object with no inherited keysInterview questions
Trie or hash set?
For exact words, a Set is simpler. Pick a trie for prefixes, wildcards or early stopping.
How would you return the top 3 autocomplete suggestions quickly?
DFS below the prefix and stop after three, or keep the best 3 on every node.
How do you delete a word?
Clear the end flag, then prune childless nodes on the way back up.
How can you make a trie smaller?
Merge single-child chains into one edge (a radix tree), or use a ternary search tree.
Check yourself
Question 1 of 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Longest Common PrefixFind the longest string that every word in a list starts with.Easy
- Implement a Trie (Prefix Tree)Build a Trie class that inserts words and answers whole-word and prefix lookups in time proportional to the query length.Medium
- Design Add and Search WordsStore words and answer searches where a dot stands for any single letter.Medium
- Replace WordsShorten every word in a sentence to the shortest dictionary root it starts with.Medium
- Word Search IIFind every word from a list that can be traced through adjacent cells of a letter grid.Hard
In real systems
This pattern is not only for coding rounds. Here is where it does the work in a design interview.