Skip to content

Trie Patterns: Mental Models & Intuition

Build deep understanding of when and why Tries work for string problems.

The Core Insight

A Trie is a tree where each path from root represents a prefix.

This simple structure enables O(L) operations where L is the string length - regardless of how many strings are stored!


Mental Model 1: The Branching Path

Imagine a filing cabinet where each drawer leads to more drawers:

                    root
                   /    \
                  a      b
                 / \      \
                p   t      a
               /     \      \
              p       e      t(end)
             /
            l
           /
          e(end)

Words: "apple", "ate", "bat"

Finding "apple":
  root β†’ a β†’ p β†’ p β†’ l β†’ e βœ“ (found: is_end=True)

Finding "app":
  root β†’ a β†’ p β†’ p β†’ NOT is_end βœ— (prefix exists, not a word)

Finding "apt":
  root β†’ a β†’ p β†’ βœ— (no 't' child)

Key insight: Shared prefixes share paths. "apple" and "application" share "appl".


Mental Model 2: The Autocomplete Tree

Think of how your phone suggests words:

You type: "m" β†’ [mobile, money, monitor]
You type: "mo" β†’ [mobile, money, monitor]
You type: "mou" β†’ [mouse, mousepad]

The Trie naturally groups words by prefix:

      m
     /|\
    o i a
   /|  |  \
  n u  c   t...
 /|  |  |
e i  s  r...
|  \  |
y   t e
|    \
pot   pad

Each node in the trie represents a "state" in typing - where you are after typing certain characters.


Mental Model 3: Wildcard as Branching

For '.' wildcard search, think of it as taking ALL possible paths:

Words: "bad", "dad", "mad"
Search: ".ad"

         root
        / | \
       b  d  m
       |  |  |
       a  a  a
       |  |  |
       d  d  d
      (end)(end)(end)

'.' at position 0 β†’ Try ALL children (b, d, m)
  Each path leads to 'a' β†’ then 'd' β†’ found!

It's like parallel universe exploration - the '.' splits reality.

For Word Search II, visualize the trie as a "validity checker":

Board:        Trie (words: ["oath", "eat"]):
o a a n            root
e t a e           /    \
i h k r          o      e
i f l v         /        \
               a          a
              /            \
             t              t
            /              (end)
           h
          (end)

DFS from 'o':
  Check: Is 'o' in trie? βœ“ β†’ Continue
  Move to 'a': Is 'a' child of 'o' in trie? βœ“ β†’ Continue
  Move to 't': Is 't' child of 'a' in trie? βœ“ β†’ Continue
  Move to 'h': Is 'h' child of 't' in trie? βœ“ + is_end β†’ Found "oath"!

Without trie: Try all 10,000 words at each cell
With trie: Follow single path, prune instantly if no match

Mental Model 5: Shortest Prefix = First End

For Replace Words, you're looking for the "first exit":

Dictionary: ["cat", "ca"]
Word: "cattle"

Trie:
root β†’ c β†’ a(end) β†’ t(end)
           ↑
           First end!

Traversing "cattle":
  'c' β†’ keep going
  'a' β†’ is_end=True! Stop here, return "ca"

The first is_end you hit is the shortest root.

Algorithm Selection Guide

Problem Type Pattern Why
Exact word lookup Basic Trie O(L) guaranteed
Prefix checking startsWith() Natural trie operation
Wildcard search DFS on Trie Branch on '.'
Multi-word grid search Trie + Backtracking Prune invalid paths
Shortest prefix Find first is_end Early termination
Autocomplete Store words at nodes O(1) per prefix query

Common Pitfalls

Pitfall 1: Confusing Prefix vs Word

# WRONG: Only checks if path exists
def search(self, word):
    node = self._find_node(word)
    return node is not None  # "app" returns True even if only "apple" exists!

# CORRECT: Check is_end flag
def search(self, word):
    node = self._find_node(word)
    return node is not None and node.is_end

Pitfall 2: Forgetting to Mark End

# WRONG: No end marker
def insert(self, word):
    node = self.root
    for char in word:
        if char not in node.children:
            node.children[char] = TrieNode()
        node = node.children[char]
    # Missing: node.is_end = True

# CORRECT: Mark word end
def insert(self, word):
    # ... same as above ...
    node.is_end = True  # Essential!
# WRONG: Can find same word multiple times
if next_node.word:
    result.append(next_node.word)
    # Word stays, might be found again from different path

# CORRECT: Clear after finding
if next_node.word:
    result.append(next_node.word)
    next_node.word = None  # Prevent duplicates
# INEFFICIENT: Keep empty branches
if next_node.word:
    result.append(next_node.word)
    next_node.word = None

# OPTIMIZED: Prune empty branches
if next_node.word:
    result.append(next_node.word)
    next_node.word = None

if not next_node.children and not next_node.word:
    del node.children[char]  # Prune dead end

Practice Progression

Level 1: Basic Operations

  1. LC 208 - Implement Trie (Insert, search, prefix)
  1. LC 211 - Add and Search Words (Wildcard with '.')

Level 3: Application

  1. LC 648 - Replace Words (Shortest prefix match)
  2. LC 1268 - Search Suggestions System (Autocomplete)

Level 4: Combination

  1. LC 212 - Word Search II (Trie + Backtracking)

Quick Reference Card

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        TRIE                              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                          β”‚
β”‚  STRUCTURE:                  INSERT:                     β”‚
β”‚  ──────────                  ───────                     β”‚
β”‚  children = {}               for char in word:           β”‚
β”‚  is_end = False                if char not in children:  β”‚
β”‚                                  create node             β”‚
β”‚                                move to child             β”‚
β”‚                              mark is_end = True          β”‚
β”‚                                                          β”‚
β”‚  SEARCH WORD:                SEARCH PREFIX:              β”‚
β”‚  ────────────                ─────────────               β”‚
β”‚  node = find_node(word)      return find_node(prefix)    β”‚
β”‚  return node and node.is_end        is not None          β”‚
β”‚                                                          β”‚
β”‚  WILDCARD '.':               GRID + TRIE:                β”‚
β”‚  ────────────                ──────────                  β”‚
β”‚  if char == '.':             Build trie from words       β”‚
β”‚    try ALL children          DFS grid, follow trie       β”‚
β”‚  else:                       Prune if no trie path       β”‚
β”‚    follow exact path         Mark visited, backtrack     β”‚
β”‚                                                          β”‚
β”‚  COMPLEXITY:                                             β”‚
β”‚  ──────────                                              β”‚
β”‚  Insert:     O(L)                                        β”‚
β”‚  Search:     O(L) exact, O(26^D Γ— L) with D wildcards   β”‚
β”‚  Space:      O(N Γ— L) for N words                        β”‚
β”‚                                                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When to Use Trie vs Alternatives

Need string prefix operations?
β”‚
β”œβ”€ No β†’ Hash Set (simpler for exact lookup)
β”‚
└─ Yes
   β”‚
   β”œβ”€ Static data, few queries? β†’ Sorted Array + Binary Search
   β”‚
   └─ Dynamic data OR many queries? β†’ Trie
      β”‚
      β”œβ”€ Need wildcards? β†’ Trie + DFS
      β”‚
      β”œβ”€ Multiple strings in grid? β†’ Trie + Backtracking
      β”‚
      └─ Need suggestions? β†’ Trie with word lists at nodes