Home › DSA Patterns › Tries
T

Tries

Tries are one of the few topics where recognizing the pattern is most of the battle. The structure is simple. The hard part is seeing one signal, "prefix," early enough to build the right thing before the clock runs out.

DSA PatternsMedium11 min read
Who asks this, and how oftenInterview frequency: Common

Most asked at Amazon. Here is how often this pattern shows up in each company's coding rounds.

Amazon
82
Google
80
Meta
78
Microsoft
72
Uber
66
Relative frequency index from 0 to 100, based on commonly reported interview experiences. Use it as a guide to where this pattern pays off most, not an exact count.

A lot of people freeze on Trie problems, and not because the structure is hard. They just do not realize a Trie was the answer until twenty minutes in. By then the whiteboard is full of a brute-force grid DFS and there is no time to pivot. The fix is to make the recognition automatic, because recognition is the whole game here.

If you remember five things

The whole pattern in one glance. Everything below defends these five calls.

  1. See "prefix," build a Trie. A set of strings plus prefix questions asked repeatedly is a Trie until proven otherwise. A single exact lookup is a hash set.
  2. Two fields per node. A map from character to child, and an is_end flag. Without the flag you cannot tell a real word from a prefix of one.
  3. Three ops, one helper. insert, search, startsWith, all built on walk. Write this cold in about 5 minutes, then spend the round on the twist.
  4. Query cost is O(L), not O(N). A prefix of length 3 touches exactly 3 nodes whether you stored ten words or ten million. That is the headline feature.
  5. Most problems are the skeleton plus one twist. Wildcard dots, a binary Trie for XOR, storing a value instead of a flag, grid DFS with pruning. Learn the base, bolt on the twist.

1The mental model (what this really is)#

A Trie is a tree where each edge is a character, and each path from the root spells out a prefix.

Picture it walking down the tree.

  • The root means "empty string."
  • Follow an edge labeled c and you are at the node for "c".
  • Follow another labeled a and you are at "ca".
  • Keep going and you spell "cat".
  • Words that share a prefix share the same path until they split.

So "cat", "car", and "card" overlap:

  • All three walk the same c -> a path.
  • "cat" branches off at t.
  • "car" and "card" stay together one more step before "card" adds a d.

Here is that split drawn out so the sharing is obvious:

graph TD root((root)) --> c[c] c --> a[a] a --> t[t end] a --> r[r end] r --> d[d end]
Notice the c -> a trunk is shared by all three words, and two nodes carry the end flag: r because "car" was inserted, and d because "card" was. The flag, not the path, is what makes a node a real word.

Each node needs exactly two things:

  • A map from character to child node.
  • A boolean flag that says "a word ends here."
Why the end flag is not optional

Without it you cannot tell whether "car" is a real inserted word or just a prefix of "card". The path existing is not the same as the word existing. Every correctness bug in this topic traces back to someone treating "the path is here" as "the word is here."

The whole point:

  • A Trie answers "does any word start with this prefix?" in time proportional to the length of the prefix.
  • It does not matter how many words you stored.
  • Store a million words, query "car", and you still touch exactly 3 nodes.
  • A hash set of strings cannot do this. It tells you if an exact string exists, but hashing "car" tells you nothing about which stored strings start with "car".

2How to recognize it (the signals in a problem)#

Train your eyes on these. When you see them, a Trie should be the first tool you reach for, before you write a single line.

  • The word prefix appears, or the problem asks about words that start with something.
  • Many lookups against a fixed dictionary of words, where plain hashing is not enough because you need partial or prefix matching.
  • Autocomplete, search suggestions, or "type-ahead" behavior.
  • Matching with wildcards where a . can mean any single character.
  • A word-search on a grid where you check many words at once against a board.
  • Finding the longest common prefix, or the shortest unique prefix, across a set of strings.
  • Anything about an XOR of numbers being maximized. (Yes, really. Bits are a 2-character alphabet over 0 and 1, so a binary Trie applies. More in section 5.)

The clearest tell is one combination:

  • A set of strings plus shared-prefix questions asked repeatedly.
  • One lookup? Use a hash set.
  • Thousands of prefix queries against the same word list? Build a Trie once, then each query is cheap.
From the interviewer's chair

When the prompt says "given a dictionary of N words" and then asks many prefix or wildcard questions, I am watching to see how fast you name the Trie. Say it out loud early. Reaching for it unprompted is worth more than a clean implementation that arrives late.

3The template (how it works, with pseudocode)#

This is the structure to write from memory. Learn this shape and you can reproduce it cold under pressure, which matters because the real problem is usually one twist bolted onto this skeleton.

class TrieNode:
    children = {}      # char -> TrieNode
    is_end = False     # true if a word ends at this node

class Trie:
    root = new TrieNode()

    insert(word):
        node = root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = new TrieNode()
            node = node.children[ch]
        node.is_end = True

    search(word):        # exact word match
        node = walk(word)
        return node != null and node.is_end

    startsWith(prefix):  # any word with this prefix?
        return walk(prefix) != null

    walk(s):             # follow s, return end node or null
        node = root
        for ch in s:
            if ch not in node.children:
                return null
            node = node.children[ch]
        return node

Three operations, one helper:

  • insert carves the path and plants the end flag.
  • walk follows a string as far as it can go, returning the node it lands on or null the moment a character is missing.
  • search is walk plus an is_end check.
  • startsWith is just walk with a null check.

If you can write these four from scratch, you can solve most Trie problems by adding one twist on top. Budget about 5 minutes to produce this clean, which leaves the rest of the round for the actual twist.

Pause before the worked example: you call search("app") on a Trie where only "apple" was inserted. What does it return, and what would the common bug return?
Reveal the answer
Correct answer is false: the path a -> p -> p exists, but no word ends there, so is_end is false. The common bug drops the is_end check and returns true because the path is present. That is the single most frequent Trie bug, and it passes toy tests while dying on the real ones. search needs the flag; startsWith does not. That one line is the only difference between them.

4A worked example (walk through one problem step by step)#

The classic: Word Search II. You are given a 2D grid of letters and a list of words. Return every word from the list that can be formed by a path of adjacent cells (up, down, left, right), using each cell at most once per word.

Why the naive approach is too slow:

  • Run a separate DFS on the grid for each word.
  • 10,000 words means 10,000 full grid searches.
  • You re-walk the same grid prefixes over and over, so "oath" and "oats" each pay the full cost of exploring o -> a -> t from scratch.

The insight:

  • Build one Trie of all the words.
  • Do a single DFS over the grid.
  • Let the Trie tell you which paths are still worth following.

Here is how I would reason through it live:

Build the Trie.

  • Insert every word. Shared prefixes become shared paths.
  • If "oath" and "oats" are both in the list, the grid search explores o -> a -> t once, not twice.

DFS from every cell.

  • Start a depth-first walk at each cell of the grid.
  • As I move to a neighbor, I move down one level in the Trie in lockstep.
  • The current grid letter must match a child in the current Trie node. If it does not, I stop immediately.
  • This is the pruning that makes it fast: the Trie kills dead branches before I waste time on them.

When a Trie node has is_end true:

  • I have spelled a complete word along this grid path.
  • Add it to the result set.

Handle the "used cell" rule.

  • Mark the current cell as visited before recursing, then unmark it when I back out.
  • Standard backtracking.

Let me trace a tiny case. Grid:

o a a n
e t a e

Words: ["eat", "oath"]. Build the Trie, two paths from the root: one down e -> a -> t, one down o -> a -> t -> h.

Start DFS at cell (1,0), which holds e.

  • Root has a child e, good, step into it.
  • Neighbors of e: o above, t to the right.
  • Trie node e has a child a, not o or t.
  • Moving to o or t fails the Trie check and gets pruned. Dead end here.
  • Correct, because "eo..." and "et..." are not prefixes of any word.

A quick sanity check on "eat":

  • From e at (1,0), the right neighbor at (1,1) is t, and there is no a adjacent below this e.
  • So this particular e cannot start "eat". The algorithm correctly moves on.
  • The grid drawn here does not actually contain "eat" as a path, which is exactly the kind of chase the Trie saves you from.

Now start DFS at (0,0) = o.

  • Root has child o. Step in.
  • Neighbor a at (0,1)? Trie node o has child a, yes. Step to a.
  • Neighbor t at (1,1)? Trie node a has child t, yes. Step to t.
  • Neighbor h? None adjacent. So "oath" is not completable here either.
  • Fine. The search was still cheap because every step was Trie-guided, and we never explored a single letter that was not on a live Trie path.

The magic:

  • One grid traversal handled all words at once.
  • The Trie pruned every path that could not lead to a real word.
  • You pay cells * 4^L once, bounded by what the Trie actually contains, instead of words * cells * 4^L.
The move that reads as senior

When you find a word, mark that Trie node's end flag as consumed (or set its stored word to null). You stop adding duplicates and can prune fully-exhausted branches. Say this out loud. It signals you have thought past the happy path.

5Variations and gotchas#

Most Trie problems are the base skeleton plus one of these. Learn the twist, not a new structure.

Wildcard search.

  • Some problems let a query contain . meaning "any one character."
  • Exact walk breaks here. The fix: when you hit a ., recurse into all children at that node instead of a single one.
  • This is the "Add and Search Word" pattern.
  • It turns a linear walk into a branching search, but only on the . positions, so a query with two dots branches twice and the rest is still a straight walk.

Binary Trie for XOR.

  • For "maximum XOR of two numbers in an array," insert each number as a 32-bit path (each bit is an edge, 0 or 1).
  • To maximize XOR for a query number, at each bit greedily go down the opposite bit's child.
  • Opposite bits XOR to 1, and a 1 in a higher bit beats anything below it.
  • This is a Trie in disguise, and interviewers love it because most people do not see the tree hiding inside a bit problem.

Storing a value, not just a flag.

  • Instead of is_end, store the full word, a frequency count, or an index.
  • Useful for "replace words with roots," autocomplete ranking, or returning the matched string directly instead of reconstructing it from the path.

Array vs hash map for children.

  • For lowercase English, a fixed array of size 26 is faster and simpler than a hash map.
  • For unicode or mixed input, use a map.
  • Say the trade-off out loud: the 26-array is cache-friendly and avoids hashing per step, but it wastes memory on sparse nodes. That one sentence signals you understand the memory cost.

Deletion.

  • Rarely asked, often botched. You do not just flip is_end off.
  • If the node has no children after unflagging, prune it upward.
  • Stop pruning at any node that is another word's end or has other children.
  • Delete "car" from a Trie that also holds "card" and you must keep the whole c -> a -> r path alive, only clearing r's end flag.

6Complexity#

Let L be the length of a word or prefix, N the number of words, and A the alphabet size (26 for lowercase English).

  • Insert: O(L) time.
  • Search / startsWith: O(L) time. Independent of how many words you stored. Query a prefix of length 3 against a million words and you still touch 3 nodes. This is the headline feature.
  • Build the whole Trie: O(total characters across all words).
  • Space: O(total characters) in the worst case, with a constant factor of A per node if you use fixed-size arrays. Shared prefixes save memory in practice, but the worst case has no sharing, so plan for it when the interviewer asks.
  • Wildcard search: worst case O(A^L) when the query is all dots, because every position branches across all children. Usually far better, since real queries have few dots.
O(L)
The number that justifies the TrieQuery cost depends only on prefix length, never on how many words you stored. Store a million, query a length-3 prefix, touch 3 nodes. A hash set matches O(L) for exact lookup but cannot answer a prefix question at all, which is the whole reason you paid for the tree.

Compare to a hash set:

  • Exact lookup is O(L) too.
  • But a hash set cannot do prefix queries at all, and that is the whole reason you paid for the Trie.
  • If the problem never asks a prefix question, the hash set wins on both space and simplicity.

7Mistakes that cost people the round#

  • Forgetting the is_end flag. Insert "apple", then search("app") returns true because the path exists. Wrong. Without the end flag you cannot distinguish a word from a mere prefix. The single most common bug, and it passes the toy tests and dies on the real ones.
  • Confusing search and startsWith. Almost identical code, differing by one is_end check. Mixing them up silently passes small tests and fails big ones, and it is a brutal bug to find when the clock is running.
  • Reaching for a Trie when a hash set is enough. If there is only exact-match lookup and no prefix question, a Trie is over-engineering. Interviewers notice when you pick the heavier tool without reason, and it reads as pattern-matching instead of thinking.
  • In Word Search II, searching per word instead of one combined grid DFS. This is the entire point of the problem. Per-word DFS is the answer they want you to improve on, so landing there and stopping is landing on the brute force they already expected.
  • Forgetting to backtrack the visited marker on grid problems. You get words that reuse a cell, and the bug is maddening to spot under pressure because the structure looks right.
  • Not pruning in wildcard search. Recursing into all children is right, but return early once you have confirmed a match or hit a dead node, otherwise you explore branches you already answered.
  • Hand-waving the node structure. Say it clearly: "each node is a map of children plus an end flag." Interviewers want to hear you name the two fields before you write any method, because it proves you have the shape in your head and are not improvising.
Red flag that fails you here

Writing return walk(word) != null for search and never adding the is_end check. It looks correct, it passes the first two examples, and it marks you as someone who has not actually run this code. Name the two fields and the one-line difference between search and startsWith before you write either.

8Your practice path (named problems by difficulty)#

Do them in this order. Each one adds exactly one new idea, so you are never learning two things at once.

Warm up (Easy to Medium):

  • Implement Trie (Prefix Tree)
  • Longest Common Prefix
  • Index Pairs of a String

Core Medium (the ones that teach the pattern):

  • Design Add and Search Words Data Structure (the wildcard . variation)
  • Replace Words (store and return the shortest root)
  • Map Sum Pairs (store values, sum over a prefix subtree)
  • Search Suggestions System (autocomplete ranking)
  • Maximum XOR of Two Numbers in an Array (the binary Trie trick)

Hard (where it all combines):

  • Word Search II (Trie + grid DFS + backtracking, the worked example above)
  • Concatenated Words
  • Stream of Characters
  • Palindrome Pairs
  • Word Squares

If you only have time for three:

  • Implement Trie, Add and Search Words, and Word Search II.
  • That trio covers build, wildcard, and the DFS combo, which is the bulk of what gets asked.
  • Spend an hour getting Implement Trie fully fluent before touching the rest, because every other problem assumes you can write that skeleton in your sleep.

9Where this shows up (which companies and rounds lean on it)#

Tries show up most in companies with search, autocomplete, or large text systems.

  • Google asks prefix and autocomplete framing often.
  • Amazon likes Word Search II and the design-a-dictionary variants.
  • Microsoft and Uber reach for the wildcard version.
  • Quant and systems-heavy shops pose the binary-Trie XOR problem to see if you can spot the hidden tree inside a bit problem.

Where it lands in the loop:

  • The standard algorithms round, most of the time.
  • Occasionally a design-flavored "build an autocomplete service" discussion, where the Trie is the storage layer under the hood.
  • If you get that design prompt, name the Trie as the prefix index, then talk about sharding it by first letter. That is exactly the move they are listening for.

See a set of strings and repeated prefix questions, build the Trie before you write a line.

Follow-ups the interviewer will ask (and your answers)
"Why not just use a hash set of all prefixes instead of a Trie?"
You can precompute every prefix of every word into a hash set, but that costs O(total characters squared) space in the worst case and still cannot enumerate "all words under this prefix" or support wildcards. The Trie gives O(L) prefix checks, shares memory across common prefixes, and the tree structure is what makes grid pruning and wildcard branching possible.
"Array of 26 or a hash map for children. Which, and why?"
For lowercase English, a fixed array of 26 is faster: no hashing per step and it is cache-friendly. The cost is memory on sparse nodes, since most nodes use a handful of the 26 slots. For unicode or large or sparse alphabets, a hash map wins on space. I state the trade-off rather than pretend one is universally right.
"How does the wildcard '.' change the complexity?"
Each `.` forces recursion into all children, so worst case is O(A^L) when the query is all dots. A query with k dots branches at k positions and walks straight through the rest, so in practice it stays close to O(L) because real queries have few wildcards. The non-dot characters still prune aggressively.
"Delete a word that is a prefix of another. What exactly do you touch?"
Clear the `is_end` flag on the deleted word's terminal node, then prune upward only while a node has no children and is not itself another word's end. Deleting `"car"` when `"card"` exists keeps the full `c -> a -> r` path because `r` still has a child `d`. I only unflag `r`, I do not remove the path.
"How is maximum XOR a Trie problem?"
Insert each number as a 32-bit path, most significant bit first, each bit an edge 0 or 1. For a query number, greedily descend into the opposite bit's child at every level, because opposite bits XOR to 1 and a 1 in a higher bit outweighs everything below. The bits are a 2-symbol alphabet, so the binary Trie is the same structure, just with branching factor 2.

What you can now explain in the room

  • The two signals that say "build a Trie": a set of strings plus repeated prefix questions.
  • Why each node needs a children map and an `is_end` flag, and what breaks without the flag.
  • The template from memory: `insert`, `search`, `startsWith`, all on top of `walk`.
  • Why a query is O(L) and independent of how many words you stored.
  • How Word Search II collapses per-word DFS into one Trie-pruned grid traversal.
  • The common twists: wildcard dots, the binary Trie for XOR, storing a value, and safe deletion.

Want to see which of these problems a specific company actually asks? Check the company guides to map this pattern to the rooms you will be sitting in.

Get a prep plan built around your Tries interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Tries prep plan is on the way.