Pattern 12 of 24 · Trees · 25 min

Tries (Prefix Trees)

Store strings character by character in a tree so prefix lookups cost O(length of word), not O(number of words).

Spot it:prefixautocompletedictionary of wordssearch many words in a gridJavaScript you'll use: Classes for data structures Map and Set Strings and characters
Say it in 30 seconds
  • 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.

0 of 0 sections ticked
30-second answer

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

Spot it in the problem0 of 3

Tap the signals you can see in the problem.

RememberprefixdictionaryWildcards

Pick your variant
  • Basic trie

    What you dochildren + 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

trie
root
Words to insert: car, cat, cart, dog, do
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
  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.
  2. Insert "car": start at the root and read it one character at a time.
  3. No edge for 'c' yet, so create a node for "c" and step into it.
  4. No edge for 'a' yet, so create a node for "ca" and step into it.
  5. No edge for 'r' yet, so create a node for "car" and step into it.
  6. Mark the last node: a word ends at "car". Without this flag, "car" would only be a prefix of other words.
  7. Insert "cat": start at the root and read it one character at a time.
  8. "c" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
  9. "ca" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
  10. No edge for 't' yet, so create a node for "cat" and step into it.
  11. Mark the last node: a word ends at "cat". Without this flag, "cat" would only be a prefix of other words.
  12. Insert "cart": start at the root and read it one character at a time.
  13. "c" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
  14. "ca" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
  15. "car" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
  16. No edge for 't' yet, so create a node for "cart" and step into it.
  17. Mark the last node: a word ends at "cart". Without this flag, "cart" would only be a prefix of other words.
  18. Insert "dog": start at the root and read it one character at a time.
  19. No edge for 'd' yet, so create a node for "d" and step into it.
  20. No edge for 'o' yet, so create a node for "do" and step into it.
  21. No edge for 'g' yet, so create a node for "dog" and step into it.
  22. Mark the last node: a word ends at "dog". Without this flag, "dog" would only be a prefix of other words.
  23. Insert "do": start at the root and read it one character at a time.
  24. "d" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
  25. "do" is already in the trie (an earlier word shares this prefix), so just follow the edge: no new node.
  26. Mark the last node: a word ends at "do". Without this flag, "do" would only be a prefix of other words.
  27. 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

  1. Pick a node shape

    Children by letter, plus an end flag.

  2. Insert every word

    Create missing children, mark the end.

  3. Decide what a query needs at the end

    A word needs the flag; a prefix needs only the path.

  4. Walk the query and stop early

    Stop when an edge is missing.

  5. Let the trie steer other searches

    Carry a trie node along; drop dead prefixes.

Interactive

Prefix tree workshop

Each node is a prefix; each edge adds one letter. Insert words and watch shared prefixes reuse nodes. Type a prefix to walk it and get completions.

rootcarttdog
Completions:
Trie lookups 2 (one per letter)Scanning all 4 words: 7 letter checksDouble ring = end of a word

4 words loaded. More are on the way: watch them grow.

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

trie
rootcarcartcarttcatdogdog

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
  1. The trie holds car, cat, cart, dog. Badged nodes end a word. Every query walks down from the root, one character per step.
  2. search("cart"): start at the root.
  3. Follow 'c' to "c".
  4. Follow 'a' to "ca".
  5. Follow 'r' to "car".
  6. Follow 't' to "cart".
  7. The whole query was read and "cart" is marked as the end of a word: search is true.
  8. search("ca"): start at the root.
  9. Follow 'c' to "c".
  10. Follow 'a' to "ca".
  11. The path "ca" exists, but no word ends there (it is only a prefix of car, cat, cart): search is false.
  12. startsWith("ca"): start at the root.
  13. Follow 'c' to "c".
  14. Follow 'a' to "ca".
  15. 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.
  16. search("cow"): start at the root.
  17. Follow 'c' to "c".
  18. "c" has no 'o' child. No stored word starts with "co", so the walk stops after 2 steps, however many words the trie holds.
  19. find returned null, so search("cow") is false.
  20. 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 asFind a childMemory per nodeWatch out for
Mapchildren.get(ch), hashingonly existing children, any characterget returns undefined, not null
plain objectchildren[ch]only existing children, any characteruse Object.create(null) against inherited keys
array of 26next[code - 97], no hashingalways 26 slots, a to z onlyother 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

trie
rootbadbadedbedcatcatodcod
pattern (3)
.
.
t
▲ i
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
  1. 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.
  2. 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.
  3. pattern[1] is a dot: try every child of "b" in turn ('a', 'e') and stop at the first that leads to a match.
  4. pattern[2] is 't', but "ba" has no 't' child. This branch is dead: go back and try the next option.
  5. pattern[2] is 't', but "be" has no 't' child. This branch is dead: go back and try the next option.
  6. Every child of "b" failed, so this call returns false.
  7. pattern[1] is a dot: try every child of "c" in turn ('a', 'o') and stop at the first that leads to a match.
  8. pattern[2] is the letter 't': there is only one edge to follow, to "cat".
  9. The whole pattern is used up at "cat", and a word ends here. Match found.
  10. 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

try-it.js
// 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
OperationTimeExtra space
Insert one wordO(L)up to L new nodes
search, startsWithO(L)O(1)
Build from all wordsO(total letters)O(total letters) nodes (each with 26 slots for arrays)
Autocomplete below a prefixO(L + size of that subtree)O(depth) for the DFS
Wildcard searchO(L) without dots; with many, every node to depth LO(L) recursion
Text scan from every positionO(n · longest word) at most, far less with pruningO(1) besides the trie

JavaScript you'll use

JavaScript refresher: Classes for data structures Map and Set Strings and characters
class 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 keys

Interview 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 3

Score 2 out of 3 or better to mark this page complete.

1. You insert "apple" into an empty trie, then call search("app") and startsWith("app"). What do they return?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

In real systems

This pattern is not only for coding rounds. Here is where it does the work in a design interview.

Next: Heaps keep the best k items at hand while data streams past.