// pattern debugger≡ menu

stack>data structure patterns / trie

// Trie (Prefix Tree)

A tree of characters where paths are prefixes — the structure for autocomplete, word dictionaries, and prefix search.

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.

c
a
r
e
insert "car" then "care" — c-a-r is shared; r and e are both end-of-word nodes

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.

  1. 01Implement TriemediumLC #208

    Insert, search, startsWith — nodes with child maps and an end-of-word flag.

  2. Trie search with '.' wildcards — DFS over all children.

  3. 03Word Search IIhardLC #212stretch

    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) or TrieNode?[26] (lowercase only, c - 'a' indexing, no hashing)
  • one bool IsWord flag per node separates "path exists" (prefix) from "path is a complete stored word" — Search needs both, StartsWith/prefix-check needs only the first
  • wildcard search = DFS that branches over every child at a . instead of one TryGetValue lookup, 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 IsWord in Search — makes a stored prefix look like a stored word
  • indexing Children[c] directly instead of TryGetValue — 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

// connections