Implement a Trie / prefix tree (Staff+ autocomplete extension)
Expected question
"Implement a Trie that supports insert(word), search(word), and startsWith(prefix)."
Variant forms
Interviewers often ask the same structure with different framing or Staff+ extensions — recognize the archetype:
- "Implement a Trie with insert, exact search, and prefix search."
- "Add autocomplete: return the top K words for a prefix."
- "How would you make concurrent reads safe while a writer updates the Trie?"
- "Support delete(word) without removing prefixes shared by other words."
- "Build a dictionary that rejects duplicate words and reports word count."
- "When is a sorted array or hash set better than a Trie?"
- "How do you reduce the memory cost of a character-per-node Trie?"
- "How would you serve typeahead from a distributed, read-heavy index?"
The question, as it might actually be asked
Implement a Trie that supports insert(word), search(word), and starts_with(prefix). Then extend it to return up to k autocomplete suggestions for a prefix. Assume lowercase words for the coding portion.
The framework
Clarify alphabet, matching semantics, and update/read concurrency → build correct prefix traversal → state time and memory costs → offer a Staff+ extension around immutable read snapshots, top-k ranking, or memory representation. Do not jump to a distributed search service before the local data structure works.
Where this actually gets asked
Common in search, marketplace, IDE, and consumer-product loops because it tests tree invariants and API boundaries. Staff follow-ups usually probe whether you recognize that autocomplete needs a ranking source and that a mutable shared Trie needs a read/write consistency contract.
Problem
Implement insert(word), search(word), and starts_with(prefix) for a Trie. Add autocomplete(prefix, k) that returns at most k lexicographically ordered matching words.
Clarifying questions you should ask first
- What alphabet and normalization rules apply: lowercase English, Unicode, or case-insensitive?
- Does
searchrequire a full word, whilestarts_withaccepts any path? (usually yes) - Should duplicate inserts be idempotent?
- What ordering or ranking defines autocomplete results?
- Are reads concurrent with writes, and may a reader see a previous complete snapshot?
Approach ladder
| Step | Idea |
|---|---|
| Brute | Store words in a list; scan every word for each prefix query — O(total characters) per query |
| Correct | Trie nodes map character→child and mark complete words — O(L) insert/search/prefix traversal |
| Staff+ | Lock writes and snapshot reads; cache top-K per node or use compressed/radix nodes when memory dominates |
Reference solution (Python)
from __future__ import annotations
from dataclasses import dataclass, field
from threading import RLock
@dataclass
class _TrieNode:
children: dict[str, "_TrieNode"] = field(default_factory=dict)
is_word: bool = False
class Trie:
"""Thread-safe Trie; readers receive a consistent traversal under one lock."""
def __init__(self) -> None:
self._root = _TrieNode()
self._lock = RLock()
def insert(self, word: str) -> None:
if not word:
raise ValueError("word must be non-empty")
with self._lock:
node = self._root
for char in word:
node = node.children.setdefault(char, _TrieNode())
node.is_word = True
def search(self, word: str) -> bool:
with self._lock:
node = self._find(word)
return node is not None and node.is_word
def starts_with(self, prefix: str) -> bool:
with self._lock:
return self._find(prefix) is not None
def autocomplete(self, prefix: str, k: int) -> list[str]:
if k < 0:
raise ValueError("k must be non-negative")
with self._lock:
start = self._find(prefix)
if start is None or k == 0:
return []
results: list[str] = []
def collect(node: _TrieNode, suffix: str) -> None:
if len(results) == k:
return
if node.is_word:
results.append(prefix + suffix)
for char in sorted(node.children):
collect(node.children[char], suffix + char)
if len(results) == k:
return
collect(start, "")
return results
def _find(self, text: str) -> _TrieNode | None:
node = self._root
for char in text:
node = node.children.get(char)
if node is None:
return None
return node
Complexity: insert/search/starts_with are O(L), where L is input length. autocomplete is O(P + visited nodes) plus sorting child keys; the structure uses O(total stored characters) nodes in the worst case.
Tests / edge cases
- Insert
apple;search("apple")is true,search("app")is false, andstarts_with("app")is true. - Insert
app; both words remain searchable and autocompleteapp,2returns["app", "apple"]. - A missing prefix returns an empty autocomplete result.
- Duplicate insert does not produce duplicate suggestions.
- Concurrent inserts and reads never expose a partially linked child path.
Staff+ deep dive
| Topic | Talking point |
|---|---|
| Read concurrency | A single lock is a correct baseline; read/write locks or immutable copy-on-write snapshots improve read-heavy workloads |
| Top-K | Lexicographic DFS is not popularity ranking; cache bounded top-K candidates per node and update them on writes |
| Memory | Dict-per-character nodes are expensive; use radix compression, array children for small fixed alphabets, or a minimal DFA offline |
| Product boundary | Query normalization, profanity filtering, tenant isolation, and ranking belong around—not inside—the basic Trie |
What's expected at each level
- Mid-level: Correct traversal and terminal-word distinction.
- Senior: Clean API, complexity, and tests for shared prefixes and missing paths.
- Staff+: Separates lexical matching from ranking, and gives a credible read/write and memory strategy.
- Principal: Connects index ownership to freshness, privacy, latency SLOs, and regional/tenant serving boundaries.
Follow-up questions to expect
- "Delete a word." — Unmark the terminal node, then prune only nodes with no children and no terminal marker.
- "Rank by popularity." — Maintain bounded ranked candidates per prefix, with a defined update and freshness policy.
- "Can readers avoid locks?" — Yes with immutable snapshots/atomic root replacement, trading write amplification for lock-free reads.