Trie Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide
A tree of characters where each path from the root spells a prefix. Prefix questions become a walk down the tree.
3 sub-patterns Β· 7 questions
What is this pattern?
A trie (prefix tree) stores many words so that words sharing a prefix share the same path. The root is the empty prefix. Each edge is one letter, and each node stands for the prefix spelled on the way down to it. A flag isEnd marks the nodes where a whole word stops.
Insert walks the word letter by letter, creating missing nodes. Search walks the same path. Either way it costs O(L) for a word of length L, no matter how many words are stored. A hash set can also check whole words in O(L), but it can't answer "does any word start with ap?" or "what is the shortest stored prefix of this word?" without trying every word. A trie answers those in one walk.
The same idea works for bits: store each number's bits from the highest down, and a walk can build the largest XOR greedily, one bit at a time.
A phone's contact search. Type J and the list shrinks to the J names; type Ja and it shrinks again. The phone never rereads every contact. It just steps one level deeper along the letters you've typed so far, and everything below that point is a match.
See it move
Circle = the letter on the way into the node; "end" = a word ends there. Green = just created, blue = the path walked, purple = found, orange = path exists but no word ends there, red = the path broke.
insert "app", "apple", "bat"; then search / startsWithstep 1/8The root stands for the empty prefix. Every node below it stands for one prefix, spelled by the letters on the way down to it.
Each level is one bit, highest first; leaf notes are the stored numbers. Green = took the opposite bit (XOR bit 1), orange = forced to take the same bit (XOR bit 0), purple = the partner found. 5 bits are shown; the real code walks 31.
nums = [3,10,5,25,2,8], x = 5step 1/7- x
- 5
- x in binary
- 00101
- xor so far
- 0
All numbers are stored bit by bit, highest bit first; each leaf's note is the number it spells. We walk down once, choosing a bit at every level.
How do I recognise it?
"Starts with", "prefix", "autocomplete", "suggestions while typing".
Many words checked against many strings (a sentence, a board): build the words once, walk once.
"Shortest root" / "longest common prefix": walk until the first or last word end.
A wildcard like
.that matches any one letter: try every child at that step."Maximum XOR" of a number with something in a set: a trie of bits, high to low.
Design a dictionary with add / search / startsWith operations.
Looks like Trie, but isn't
If you never ask about prefixes, an unordered_set<string> does the same O(L) lookup with far less code and memory.
With a single word there is nothing to share. Plain DFS from each cell is enough; the trie pays off when many words are searched at once (Word Search II).
Exact matches (a ^ b == k) only need a hash map of seen values. The bitwise trie is for maximising (or bounding) a XOR, where the greedy walk matters.
How would I discover it from scratch?
Don't memorise βuse trieβ. Ask these questions in order β the pattern falls out.
Compare the query with every stored word: O(N Β· L) per query. With many queries, the same prefix comparisons are repeated again and again.
Sub-patterns
Same core idea, different rule for βvalidβ. Learn them in this order.
Prefix tree basics
3 questionsBuild a trie of words, then answer prefix questions by walking it: exists, shortest prefix, or suggestions while typing.
Trie + DFS search
2 questionsWhen a query can branch (wildcards, a board of letters), run a DFS that walks the trie at the same time and stops as soon as no stored word continues the path.
Bitwise trie
2 questionsStore numbers as bit paths, highest bit first. To maximise a XOR, walk down preferring the opposite bit at each level.
Don't mix these up
Both walk the same path. search also needs the last node's end flag; startsWith doesn't. After inserting apple: startsWith("app") is true, search("app") is false.
Only whole-word lookups? Use the set. Any question about prefixes (starts with, shortest root, autocomplete, prune a search early) is where the trie wins.
In [3,10,5,25,2,8], the two largest give 25 ^ 10 = 19, but 5 ^ 25 = 28. What matters is having different bits at the highest possible position, which the bitwise trie finds.
Code templates (C++)
Trie: insert, search, startsWith
One walk helper serves both queries: search needs the path and isEnd; startsWith needs only the path.
// Prefix tree: one node per prefix; children indexed by letter.
struct TrieNode {
TrieNode* child[26] = {};
bool isEnd = false; // a whole word ends here (not just a prefix)
};
class Trie {
TrieNode* root = new TrieNode();
// Walks the path for s; returns the last node, or nullptr if the path breaks.
TrieNode* walk(const string& s) {
TrieNode* cur = root;
for (char c : s) {
cur = cur->child[c - 'a'];
if (!cur) return nullptr;
}
return cur;
}
public:
void insert(const string& word) {
TrieNode* cur = root;
for (char c : word) {
if (!cur->child[c - 'a']) cur->child[c - 'a'] = new TrieNode();
cur = cur->child[c - 'a'];
}
cur->isEnd = true;
}
bool search(const string& word) { TrieNode* n = walk(word); return n && n->isEnd; }
bool startsWith(const string& prefix) { return walk(prefix) != nullptr; }
};Trie + DFS for wildcards
A normal letter follows one child; . tries every existing child. A broken path returns false immediately, which prunes the search.
// Trie + DFS: when one letter can branch (a wildcard, a grid neighbour),
// try every child, but only along paths that exist in the trie.
// Here: search with '.' matching any one letter (LC 211).
struct TrieNode {
TrieNode* child[26] = {};
bool isEnd = false;
};
bool match(TrieNode* node, const string& pat, int i) {
if (!node) return false; // the path broke: no word continues this way
if (i == (int)pat.size()) return node->isEnd;
if (pat[i] != '.') return match(node->child[pat[i] - 'a'], pat, i + 1);
for (TrieNode* next : node->child) // '.': any existing child may continue the match
if (next && match(next, pat, i + 1)) return true;
return false;
}Bitwise trie: maximum XOR
Insert bits from 30 down to 0. To maximise x ^ y, prefer the child with the opposite bit at every level; take the same bit only when forced.
// Bitwise trie: store each number's bits from the highest down.
// For the largest XOR with x, prefer the OPPOSITE bit at every level: one higher bit
// is worth more than all lower bits together (2^b > 2^b - 1).
struct BitNode {
BitNode* child[2] = {};
};
const int HIGH = 30; // values < 2^31
void insert(BitNode* root, int x) {
BitNode* cur = root;
for (int b = HIGH; b >= 0; b--) {
int bit = (x >> b) & 1;
if (!cur->child[bit]) cur->child[bit] = new BitNode();
cur = cur->child[bit];
}
}
// Largest x XOR y over all y in the trie (the trie must not be empty).
int bestXor(BitNode* root, int x) {
BitNode* cur = root;
int result = 0;
for (int b = HIGH; b >= 0; b--) {
int want = 1 - ((x >> b) & 1); // the bit that makes this XOR bit 1
if (cur->child[want]) { result |= 1 << b; cur = cur->child[want]; }
else cur = cur->child[1 - want];
}
return result;
}