core idea
A trie is a tree where edges spell characters and root-to-node paths spell prefixes.
Insert "car" and "care" and they share the nodes for c, a, r — only e is new. Every
node is a tiny dictionary of “what character can I go to next,” so the cost of a lookup is the
length of the string, never the number of words stored. That’s the whole trade you’re making:
pay per character inserted, get prefix search for free.
The one real design decision is how a node stores its children:
| approach | lookup | memory | when |
|---|---|---|---|
Dictionary<char, TrieNode> |
O(1) avg, hashing cost | one entry per child that actually exists | any alphabet, sparse branching, wildcard search |
TrieNode?[26] |
O(1), no hashing | 26 slots per node, mostly null |
lowercase a-z only, you want the fastest possible walk |
when to reach for it
- The problem statement says prefix, starts with, or ships a dictionary of words you’ll query repeatedly.
- You need autocomplete-style lookups: “what words begin with this prefix?”
- A wildcard character (
.meaning “any letter”) appears in the search — a trie turns that into a small DFS over children instead of testing every stored word. - Many words share long prefixes and you’d be repeating work with a plain
HashSet<string>.
universal templates
Dictionary-based node — the default, works for any alphabet:
public class TrieNode
{
public Dictionary<char, TrieNode> Children { get; } = []; // sparse: pay only for chars used
public bool IsWord; // true at nodes that end a word
}
public class Trie
{
private readonly TrieNode _root = new();
public void Insert(string word)
{
var node = _root;
foreach (char c in word)
{
if (!node.Children.TryGetValue(c, out var next))
node.Children[c] = next = new TrieNode(); // extend the tree on the first miss
node = next;
}
node.IsWord = true; // mark the path we just walked
}
public bool Search(string word)
{
var node = _root;
foreach (char c in word)
{
if (!node.Children.TryGetValue(c, out var next)) return false; // path breaks → not stored
node = next;
}
return node.IsWord; // path exists, but is it a full word?
}
}
Array-based node — same shape, a-z only, no hashing:
public class TrieNode
{
public TrieNode?[] Children { get; } = new TrieNode?[26]; // index c - 'a' directly
public bool IsWord;
}
public class Trie
{
private readonly TrieNode _root = new();
public void Insert(string word)
{
var node = _root;
foreach (char c in word)
node = node.Children[c - 'a'] ??= new TrieNode(); // create-if-missing in one line
node.IsWord = true;
}
public bool Search(string word)
{
var node = _root;
foreach (char c in word)
{
node = node.Children[c - 'a'];
if (node is null) return false;
}
return node.IsWord;
}
}
Every problem below is Insert plus a walk over Children — search, wildcard search, and grid
backtracking are all the same walk with a different stopping rule.
the one question to ask
Before picking a representation, ask: “is my alphabet small, fixed, and dense — or could it be anything?” Interview problems are almost always lowercase English, so the array is the idiomatic choice when performance is on the table. Reach for the dictionary the moment the alphabet is uppercase-and-lowercase, includes digits, or you’re not sure — it costs nothing to be safe.
problems
Three problems, one structure: build it, search it with a wildcard, then run a grid search through it.
Insert, search, startsWith — nodes with child maps and an end-of-word flag.
Trie search with '.' wildcards — DFS over all children.
Trie + grid backtracking. Prerequisite: Word Search (backtracking).
cheat sheet — trie
recognize it
- problem statement says "prefix", "starts with", or hands you a dictionary/list of words to query repeatedly
- autocomplete-style question: "what words begin with X?"
- a wildcard character (e.g.
.= any letter) appears inside a word-search question - many words share long prefixes and a
HashSet<string>would repeat the same scan work
key tricks
- node = tiny dictionary of "what char can I go to next":
Dictionary<char, TrieNode>(any alphabet) orTrieNode?[26](lowercase only,c - 'a'indexing, no hashing) - one
bool IsWordflag per node separates "path exists" (prefix) from "path is a complete stored word" —Searchneeds both,StartsWith/prefix-check needs only the first - wildcard search = DFS that branches over every child at a
.instead of oneTryGetValuelookup, short-circuiting the moment a branch returns true - Word Search II: store the whole word (or null) at its terminal node, add it to results and null the field on match — that's how you avoid reporting the same word twice across different grid paths
- cost of every trie op is O(word length), not O(number of words stored) — that's the entire reason it beats a flat list/set
common bugs
- checking only "does the path exist" and skipping
IsWordinSearch— makes a stored prefix look like a stored word - indexing
Children[c]directly instead ofTryGetValue— throws or silently auto-creates a node you didn't mean to - in wildcard search, not short-circuiting the
.branch loop (or ANDing instead of ORing branch results) — either misses valid matches or wastes work exploring every child - Word Search II: rebuilding the trie per grid cell instead of once up front, or forgetting to null the matched word's field — duplicate results or O(cells × words) waste
- Word Search II specifically: skipping the mark-visited-before-recurse / restore-after-backtrack discipline from plain Word Search — the trie on top makes that bug much harder to spot