Tries
A trie (prefix tree) is a tree where every node represents a character and paths spell out strings. They’re the right data structure for autocomplete, spell-check, IP routing tables (radix trie), and any “does any stored string have this prefix?” problem — faster than a hash map for prefix queries.
Implementation + autocomplete + radix
EXAMPLE
// 1) Basic implementation — array-of-children variant for tiny alphabets
class TrieNode {
constructor() {
this.children = new Map(); // char -> TrieNode (or Array for ASCII only)
this.isEnd = false; // marks the end of an inserted word
this.count = 0; // number of words passing through this node
}
}
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.count++;
}
node.isEnd = true;
}
contains(word) {
const node = this._find(word);
return !!node && node.isEnd;
}
startsWith(prefix) {
return this._find(prefix) !== null;
}
_find(s) {
let node = this.root;
for (const ch of s) {
node = node.children.get(ch);
if (!node) return null;
}
return node;
}
}
// 2) Use it
const trie = new Trie();
['apple', 'app', 'application', 'apt', 'art'].forEach((w) => trie.insert(w));
trie.contains('app'); // true
trie.contains('ap'); // false — 'ap' is a prefix, not an inserted word
trie.startsWith('app'); // true
// 3) Autocomplete — collect all words with a prefix
function wordsWithPrefix(trie, prefix) {
const node = trie._find(prefix);
if (!node) return [];
const out = [];
(function dfs(n, current) {
if (n.isEnd) out.push(current);
for (const [ch, child] of n.children) dfs(child, current + ch);
})(node, prefix);
return out;
}
wordsWithPrefix(trie, 'app'); // ['app', 'apple', 'application']
// 4) Top-K autocomplete with frequency counts
class FreqTrie {
constructor() { this.root = new TrieNode(); }
insert(word, freq = 1) {
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;
node.freq = (node.freq ?? 0) + freq;
}
topK(prefix, k = 5) {
const start = this._find(prefix);
if (!start) return [];
const results = [];
(function dfs(n, cur) {
if (n.isEnd) results.push({ word: cur, freq: n.freq });
for (const [ch, child] of n.children) dfs(child, cur + ch);
})(start, prefix);
return results.sort((a, b) => b.freq - a.freq).slice(0, k);
}
_find(s) {
let node = this.root;
for (const ch of s) { node = node.children.get(ch); if (!node) return null; }
return node;
}
}
const ac = new FreqTrie();
for (const w of ['apple', 'apple', 'app', 'application', 'apt']) ac.insert(w);
ac.topK('app', 3); // [{word: 'apple', freq: 2}, {word: 'app', freq: 1}, ...]
// 5) Delete a word — keep the trie clean
function deleteWord(trie, word) {
function helper(node, i) {
if (i === word.length) {
if (!node.isEnd) return false;
node.isEnd = false;
return node.children.size === 0;
}
const ch = word[i];
const child = node.children.get(ch);
if (!child) return false;
const shouldDelete = helper(child, i + 1);
if (shouldDelete) {
node.children.delete(ch);
return !node.isEnd && node.children.size === 0;
}
return false;
}
helper(trie.root, 0);
}
// 6) Word search (Leetcode 79 / 212) — trie + DFS on a grid
function findWords(board, words) {
const trie = new Trie();
for (const w of words) trie.insert(w);
const R = board.length, C = board[0].length;
const result = new Set();
function dfs(r, c, node, path) {
if (r < 0 || c < 0 || r >= R || c >= C) return;
const ch = board[r][c];
if (ch === '#') return;
const child = node.children.get(ch);
if (!child) return;
const next = path + ch;
if (child.isEnd) result.add(next);
board[r][c] = '#'; // mark visited
dfs(r+1, c, child, next);
dfs(r-1, c, child, next);
dfs(r, c+1, child, next);
dfs(r, c-1, child, next);
board[r][c] = ch; // backtrack
}
for (let r = 0; r < R; r++)
for (let c = 0; c < C; c++)
dfs(r, c, trie.root, '');
return [...result];
}
// 7) Radix tree (compressed trie) — each edge stores a STRING, not a single char
// Used for routing tables (IP prefix matching), DNS, web framework routers (Echo, gin).
// Insertion: walk; at a mismatch, split the edge into common prefix + two diverging children.
// 8) Persistent trie — Patricia / HAMT / immutable map
// Clojure / Scala / Immer use persistent tries to implement immutable maps with O(log32 n) ops
// (HAMT — Hash Array Mapped Trie).
// 9) Trie vs alternative data structures
// contains(s) startsWith(p) space
// Hash set O(|s|) no O(N*L)
// Sorted array O(|s| log N) O(|p| log N) O(N*L)
// Trie O(|s|) O(|p|) O(total unique characters)
// BloomFilter O(k) no very small but probabilistic
//
// Trie wins for prefix queries, autocomplete, and big shared prefixes (URLs, file paths).
// 10) Memory considerations
// • Children stored as Map — flexible, larger constant factor
// • Children stored as Array (26 slots for lowercase ASCII) — fast lookup, wasted memory if sparse
// • Pointers in JavaScript ≈ 8 bytes per child — at scale, use a typed array layout in C++/Rust
// • Compress with a radix trie when many nodes have a single child (URL paths)
// 11) Real-world uses
// • Autocomplete + spell check
// • IP routing tables — longest-prefix match
// • DNS server zone lookups
// • HTTP routers (gin, echo) — radix trees over URL paths
// • Predictive text (T9 keyboards)
// • Suffix tries / suffix arrays for substring search (advanced — Ukkonen's algorithm)
// • Genome sequence search
// 12) Persistence on disk — Marisa-trie, succinct trie
// For huge dictionaries (millions of words), libraries store tries in compact, mmappable form.
// Look for: marisa-trie (Python), datrie, libcst (Hat-trie), HAT-trie / louds-trie research.
// 13) Common bugs
// • Forgetting isEnd → prefix matches succeed but exact matches return false
// • Reusing nodes across inserts — implementation bug; each new char in a new word allocates
// • Mutating the board in word-search without backtracking — corrupts subsequent searches
// • Memory blow-up on emoji or wide Unicode — use code-point iteration, not UTF-16 code units
// • Sorting on each autocomplete query — cache top-K per prefix node if reads outpace writes
// • Treating delete as 'set isEnd = false' only — leaks unused nodes; clean up with the recursive pattern above
// • Building a trie when a hash set works — only switch when prefix queries dominate
Why it matters
Reach for a trie when prefix queries dominate — autocomplete, longest-prefix match (IP routing), wildcard search across a dictionary. A radix trie compresses single-child chains into edge strings and is the structure powering most modern HTTP routers; for huge static dictionaries, look at succinct / mmap-friendly variants.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
class Trie {
constructor() { this.root = {}; }
insert(w) {
let n = this.root;
for (const c of w) n = (n[c] ??= {});
n.end = true;
}
has(w) {
let n = this.root;
for (const c of w) { if (!n[c]) return false; n = n[c]; }
return !!n.end;
}
}
Try it Yourself »
Discussion
Loading…