HARD Lab · Tries · 73

208. Implement Trie (Prefix Tree)

Build a prefix tree where each node holds a map of child characters and an isEnd flag — then insert, search, and startsWith all reduce to walking that path one character at a time.

MediumTriePrefix TreeTypeScript

PROBLEM What we're building

Implement a Trie with three methods. insert(word) stores a word; search(word) returns true only if the exact word was inserted; startsWith(prefix) returns true if any stored word begins with that prefix. After insert("apple"): search("apple")→true, search("app")→false, startsWith("app")→true. Then insert("app") makes search("app")→true.

KEY IDEA A tree of characters, with an end-of-word flag

Insight → a trie is a tree where edges are characters and each node owns a children map from a character to the next node. Shared prefixes share nodes, so "app" and "apple" reuse the same first three nodes. The single extra bit per node — isEnd — is what distinguishes a stored word from a mere prefix. That one flag is the entire difference between search and startsWith.

RECIPE Walk the path; create on insert, fall off on miss

  • Node shape. Each TrieNode has children: Record<string, TrieNode> and a boolean isEnd, because we must both branch by character and record where words terminate.
  • insert. Start at the root; for each char, create the child if absent, then descend. Mark the final node isEnd = true — that is what makes it a real word and not just a passed-through prefix.
  • walk (shared helper). For each char, if the child is missing return null (the path fell off the tree); otherwise descend. Return the node you land on. Both reads use this.
  • search vs startsWith. Both walk the path. search additionally requires node.isEnd (a complete word), while startsWith is happy with any surviving path. Same walk, different finish line.
Classic confusion → search("app") returns false even after insert("apple"), because the node at "app" exists but its isEnd is still false. Forgetting the isEnd check makes search behave like startsWith — a very common bug.

COST Complexity & alternatives

Hash set of words
O(1) search
But prefix queries cost O(N·L) — scan every word.
Trie
O(L) per op
L = key length, independent of how many words are stored.

Why a trie

Every operation costs O(L) in the length of the key — not the number of stored words — which is exactly why prefix queries are fast. Space is O(total characters) across all inserted words, shared along common prefixes. A plain hash set answers exact search in O(L) too, but cannot answer startsWith without scanning everything.

Pattern transfer → the same node-walk powers Word Search II (a trie of the dictionary pruning a DFS on the board), Add and Search Word (a . wildcard branches into all children), Design Add/Search Words Data Structure, and autocomplete / longest-common-prefix problems.

RUN IT Walk the prefix tree char by char

step 0 / 26
STARTFresh trie: a single empty root node with no children. Run each operation in order.
opsinsert appleinsert appsearch appsearch apstartsWith ap
active charwalkedchild found / isEndmissing child / false
slowfast

TYPESCRIPT The solution, annotated

trie.ts
class TrieNode {
  children: Record<string, TrieNode> = {};
  isEnd = false;
}

class Trie {
  private root = new TrieNode();

  insert(word: string): void {
    let node = this.root;
    for (const c of word) {            // walk, creating as we go
      if (!node.children[c]) node.children[c] = new TrieNode();
      node = node.children[c];
    }
    node.isEnd = true;                 // mark the terminal node
  }

  search(word: string): boolean {
    const node = this.walk(word);
    return node !== null && node.isEnd; // full word ⇒ must be a terminal
  }

  startsWith(prefix: string): boolean {
    return this.walk(prefix) !== null;  // any path is enough
  }

  // Walk the path for s; return the node it ends at, or null if it falls off.
  private walk(s: string): TrieNode | null {
    let node = this.root;
    for (const c of s) {
      if (!node.children[c]) return null;
      node = node.children[c];
    }
    return node;
  }
}

Reading it block by block

The node. A TrieNode is just a children map (char → node) plus an isEnd boolean. No character is stored in the node — the character is the edge(the key in the parent's map).
insert. Walk from the root, creating each missing child as you go, then descend. After the loop you are sitting on the node that represents the whole word — set isEnd = true there.
walk helper.Shared by both reads: for each character, a missing child means the path isn't in the tree → return null. Otherwise return the final node.
search. Walk the word; it's present only if the walk survived and the landing node has isEnd true. The isEnd check is what stops a prefix from counting as a word.
startsWith. Identical walk, but any surviving node suffices — we never look at isEnd. If the path exists, some word goes through it.
Complexity → Each of insert / search / startsWith is O(L) in the key length L. Total space is O(Σ word lengths), shared across common prefixes (worst case if no prefixes are shared).

INTERVIEWFollow-ups they'll ask

  • "Lowercase a–z only — optimize?" Swap the map for a fixed TrieNode[26] array indexed by c - 'a' for less overhead per node.
  • "Add a delete method?" Walk to the end, clear isEnd, then prune nodes bottom-up while they have no children and aren't themselves a word.
  • "Support a '.' wildcard in search?" When you hit ., recurse into every child (this is LC 211).
  • "Find all words with a prefix (autocomplete)?" Walk to the prefix node, then DFS collecting every isEnd below it.
  • "Why not just a HashSet?"A set gives O(1) exact lookup but can't do prefix queries without scanning all keys; the trie's shared structure is the point.

MNEMONIC The one-liner

"Each node = a char-map plus an isEnd bit; walk to insert (creating), walk to read (search needs isEnd, startsWith doesn't)."

TRIGGERS When you see ___ → reach for ___

prefix / startsWith / autocompletetrie (prefix tree)
distinguish full word from prefixisEnd flag on the node
dictionary pruning a grid DFStrie of words (Word Search II)
"." wildcard in searchrecurse into all children

SKELETON The reusable shape

skeleton.ts
class TrieNode {
  children: Record<string, TrieNode> = {};
  isEnd = false;
}
class Trie {
  root = new TrieNode();
  insert(w: string) {
    let n = this.root;
    for (const c of w) n = (n.children[c] ??= new TrieNode());
    n.isEnd = true;
  }
  private walk(s: string) {
    let n = this.root;
    for (const c of s) { if (!n.children[c]) return null; n = n.children[c]; }
    return n;
  }
  search(w: string) { const n = this.walk(w); return !!n && n.isEnd; }
  startsWith(p: string) { return this.walk(p) !== null; }
}

FLASHCARDS Tap to flip

What does a TrieNode hold?
A children map (char → TrieNode) and an isEnd boolean. The character lives on the edge, not in the node.
tap to flip
1 / 6
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What two things does a TrieNode store?
QUESTION 02
After insert("apple") only, what does search("app") return?
QUESTION 03
The only behavioral difference between search and startsWith is:
QUESTION 04
Time complexity of insert/search/startsWith for a key of length L?
QUESTION 05
During a search walk, you reach a character whose child is missing. You should:
QUESTION 06
Main advantage of a trie over a HashSet of the words?
QUESTION 07
To support a "." wildcard that matches any single character in search: