Trie Structure
root / \ a b | | p a / \ | p e* d* | l | e*

Words: "apple" (a→p→p→l→e*), "ape" (a→p→e*), "bad" (b→a→d*). * marks word end.

Insert: O(L) — create nodes for each char
Search: O(L) — follow char path
Prefix: O(L) — same as search, no isEnd check
Standard Trie Implementation
C++ struct TrieNode { TrieNode* children[26] = {}; bool isEnd = false; }; class Trie { TrieNode* root = new TrieNode(); public: void insert(string word) { TrieNode* cur = root; for(char c : word) { int i = c-'a'; if(!cur->children[i]) cur->children[i] = new TrieNode(); cur = cur->children[i]; } cur->isEnd = true; } bool search(string word) { TrieNode* cur = root; for(char c : word) { int i = c-'a'; if(!cur->children[i]) return false; cur = cur->children[i]; } return cur->isEnd; } bool startsWith(string prefix) { TrieNode* cur = root; for(char c : prefix) { int i = c-'a'; if(!cur->children[i]) return false; cur = cur->children[i]; } return true; } }; // Insert/Search/StartsWith: O(L) time, O(L) space per insert
Interactive: Trie Search

Search for "ape" in a trie containing "apple" and "ape". Each cell is one character in the query — traverse the trie char by char, O(L) total regardless of dictionary size.

Practice Problems
#208Implement Trie (Prefix Tree)Medium
Implement a trie with insert, search, and startsWith operations.
Pattern: Use the standard Trie implementation shown above. Each node has 26 children pointers.
C++ // Use the Trie class defined above — it IS the solution // Time per op: O(L) Space: O(total_chars)
#211Design Add and Search Words Data StructureMedium
Like Trie, but search supports '.' as wildcard matching any letter.
Pattern: Modified Trie. When searching and char is '.', try all 26 children recursively.
C++ struct Node { Node* ch[26]={}; bool end=false; }; class WordDictionary { Node* root=new Node(); bool dfs(Node* n, string& w, int i) { if(i==w.size()) return n->end; if(w[i]!='.') { int c=w[i]-'a'; return n->ch[c] && dfs(n->ch[c],w,i+1); } for(int c=0;c<26;c++) if(n->ch[c] && dfs(n->ch[c],w,i+1)) return true; return false; } public: void addWord(string w) { Node* cur=root; for(char c:w) { if(!cur->ch[c-'a']) cur->ch[c-'a']=new Node(); cur=cur->ch[c-'a']; } cur->end=true; } bool search(string w) { return dfs(root,w,0); } };
#421Maximum XOR of Two Numbers in an ArrayMedium
Find the maximum value of nums[i] XOR nums[j] over all pairs.
Pattern: Binary trie — children are bits 0/1 instead of letters. Insert every number MSB-first, then for each number walk the trie greedily taking the opposite bit whenever possible (opposite bits set 1s in the XOR from the top down).
C++ struct Node { Node* ch[2]={}; }; int findMaximumXOR(vector<int>& nums) { Node* root=new Node(); for(int x:nums) { // insert, MSB first Node* cur=root; for(int b=30;b>=0;b--) { int bit=(x>>b)&1; if(!cur->ch[bit]) cur->ch[bit]=new Node(); cur=cur->ch[bit]; } } int best=0; for(int x:nums) { // query: prefer opposite bit Node* cur=root; int v=0; for(int b=30;b>=0;b--) { int want=((x>>b)&1)^1; if(cur->ch[want]) { v|=(1<<b); cur=cur->ch[want]; } else cur=cur->ch[want^1]; } best=max(best,v); } return best; } // Time: O(n·31) Space: O(n·31)
#212Word Search IIHard
Find all words from a dictionary that exist in a 2D board. Can't reuse cells.
Pattern: Build Trie from words. DFS from each cell, follow Trie. When isEnd=true, found a word. Prune when no Trie path exists.
C++ struct Node { Node* ch[26]={}; string word; }; void dfs(vector<vector<char>>& b, Node* n, int r, int c, vector<string>& res) { if(r<0||r>=b.size()||c<0||c>=b[0].size()||b[r][c]=='#') return; int i=b[r][c]-'a'; if(!n->ch[i]) return; n=n->ch[i]; if(!n->word.empty()) { res.push_back(n->word); n->word=""; } char tmp=b[r][c]; b[r][c]='#'; dfs(b,n,r+1,c,res); dfs(b,n,r-1,c,res); dfs(b,n,r,c+1,res); dfs(b,n,r,c-1,res); b[r][c]=tmp; } vector<string> findWords(vector<vector<char>>& board, vector<string>& words) { Node* root=new Node(); for(auto& w:words) { Node* cur=root; for(char c:w) { if(!cur->ch[c-'a']) cur->ch[c-'a']=new Node(); cur=cur->ch[c-'a']; } cur->word=w; } vector<string> res; for(int r=0;r<board.size();r++) for(int c=0;c<board[0].size();c++) dfs(board,root,r,c,res); return res; }
Trie vs HashMap: HashMap can do O(L) prefix check too, but Trie is better for: prefix enumeration, pattern matching with wildcards, XOR-based maximum (binary trie).
Deeper Understanding

🧠 Under the Hood — shared prefixes are stored once

Trie: "apple" + "ape" share "ap" · a p p… e → "apple" ✓ end "ape" Hash set: no shared structure "apple" → hash 0x3f2 "ape" → hash 0x9a1 Membership: O(L) ✓ Enumerate all "ap…"-words: full scan ✗

A trie is a tree where the path spells the key — the node itself stores almost nothing. That's why prefix operations are free: all words starting with "ap" live in exactly one subtree, so autocomplete is "walk L nodes, then DFS the subtree". A hash set scatters related keys uniformly (by design!), destroying prefix locality — great for exact membership, useless for enumeration.

⚖️ When to Use / When NOT to Use

✅ Use when you need prefix queries: autocomplete, startsWith, "count words with prefix"
The prefix IS a node — everything below it matches, O(L) to find.
✅ Use when matching many patterns against text simultaneously (Word Search II)
One trie walk advances all dictionary words at once, and dead branches prune the DFS.
✅ Use when maximizing XOR — a binary trie over bits, MSB first
Greedy bit-by-bit choice needs "does a number with this bit-prefix exist?", exactly what a trie answers.
❌ Avoid when you only need exact membership or key→value lookup
Instead: unordered_set/map — same O(L), far less memory, better cache behavior.

🚫 Common Misconceptions

"Tries save memory because prefixes are shared"
Usually the opposite: every node carries 26 pointers (208 bytes each on 64-bit). Sharing helps only with heavily overlapping keys. For memory, use `array[26]` → `unordered_map<char,Node*>` or a compressed (radix) trie.
"Search hitting a node means the word exists"
Reaching 'e' in "ape" only proves the prefix exists — the isEnd flag is what distinguishes a stored word from a prefix of one. Forgetting it is the #1 trie bug.
"Trie operations are O(1) because depth is bounded"
They're O(L) in the key length — for long strings that's the same as hashing the string. The win is what happens after the walk (enumeration, wildcards), not the walk itself.

🎤 Interview Follow-ups

"array[26] children or unordered_map — trade-offs?"
Array: O(1) child access, cache-friendly, 26 pointers even for leaf-heavy tries. Map: memory proportional to actual children, slower constant, needed for Unicode alphabets. State the alphabet-size assumption first.
"How do you delete a word from a trie?"
Unset isEnd, then walk back up freeing nodes that have no children and no isEnd — easiest recursively, returning "can prune" up the call stack. Reference counting per node also works.
"Why insert MSB-first in the XOR trie?"
XOR maximization is greedy from the most significant bit — a 1 at bit 30 beats 1s at all lower bits combined (2³⁰ > 2³⁰−1). MSB-first ordering lets the trie walk make that greedy choice level by level.