Trie

Medium
Time: O(L) per operation where L = word length, O(M*N*4^L) for Word Search IISpace: O(N * L) where N = words, L = avg length
0/6
problems solved
0%
1.

What is a Trie?

A Trie (pronounced "try", from retrieval) is a tree-like data structure for storing strings where each node represents a character.

The Phone Book Analogy: Imagine a phone book organized by first letter, then second letter, etc.:

  • Looking up "Smith" → S section → Sm section → Smi → Smit → Smith
  • Once you're in the "Sm" section, you've eliminated all names not starting with "Sm"

Visual Structure:

(root)
/ \
a b
/ \ \
p n a
/ \ \ \
p e t* d*
/ \
l* (end)*
e
(end)*
Words: "apple", "ape", "ant", "bad"
* = isEnd marker (complete word)

Key Properties:

  • Root is empty (represents empty prefix)
  • Each edge represents a character
  • Path from root = prefix
  • isEnd flag marks complete words
  • Shared prefixes share nodes (memory efficient for similar words)
2.

Why Tries Matter in Interviews

Tries solve prefix problems that HashMap cannot efficiently handle.

The Killer Feature: Prefix Operations

HashMap: Trie:
contains("app") → O(1) search("app") → O(3)
startsWith("app") → O(N)! startsWith("app") → O(3)!
autocomplete("app") → O(3 + results)

What Interviewers Test:

  1. Data Structure Design - Can you implement from scratch?
  2. Prefix Understanding - Why Trie over HashMap?
  3. Combining Patterns - Trie + DFS/BFS/Backtracking
  4. Optimization Thinking - Array vs HashMap children

Frequency in Interviews:

  • Google, Facebook: Very common for autocomplete systems
  • LeetCode: ~15 Trie problems, several are hard
  • Most common: Implement Trie, Word Search II, Autocomplete

Real-World Uses:

  • Autocomplete/Typeahead
  • Spell checkers
  • IP routing (longest prefix match)
  • Phone directories
  • DNA sequence matching
3.

The TrieNode Structure

Before implementing Trie, understand the node structure.

Two Common Implementations:

1. Array-based (fixed alphabet):

class TrieNode {
TrieNode[] children = new TrieNode[26]; // a-z
boolean isEnd = false;
}
  • Pro: O(1) child access
  • Con: Wastes space if few chars used
  • Best for: lowercase English letters only

2. HashMap-based (flexible):

class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
boolean isEnd = false;
}
  • Pro: Memory efficient for sparse alphabets
  • Con: HashMap overhead
  • Best for: Unicode, mixed case, large alphabets

Visual Comparison:

Array-based for "cat": HashMap-based for "cat":
children[2] → TrieNode children{'c' → TrieNode}
(index = 'c' - 'a' = 2) (key = 'c')

Additional Node Data:

class TrieNode {
TrieNode[] children;
boolean isEnd;
String word; // Store complete word at end
int count; // Count of words with this prefix
}
4.

Implementing Insert

Inserting a word creates the path if it doesn't exist.

Algorithm:

  1. Start at root
  2. For each character:
    • If child doesn't exist, create it
    • Move to child
  3. Mark final node as word end

Visual Trace - Insert "cat" then "car":

Insert "cat":
root → (create 'c') → (create 'a') → (create 't', mark isEnd)
root
|
c
|
a
|
t*
Insert "car":
root → c (exists) → a (exists) → (create 'r', mark isEnd)
root
|
c
|
a
/ \
t* r*
* = isEnd

Key Insight: Shared prefixes share nodes. "cat" and "car" share "ca".

Code
Java
5.

Search vs StartsWith - The Critical Difference

The most important distinction in Trie operations.

search(word): Does this EXACT word exist? startsWith(prefix): Does ANY word start with this prefix?

Example with Trie containing: ["apple", "app", "application"]

root
|
a
|
p
|
p* ← isEnd ("app" is complete word)
|
l
|
e* ← isEnd ("apple" is complete word)
\
... ("application" continues)

Results:

search("app") → true ("app" exists AND isEnd=true)
search("appl") → false (path exists but isEnd=false)
search("apple") → true (exists AND isEnd=true)
search("apply") → false (path doesn't exist)
startsWith("app") → true (path exists)
startsWith("appl") → true (path exists)
startsWith("apple") → true (path exists)
startsWith("apply") → false (path doesn't exist)

The Only Difference: search checks isEnd, startsWith doesn't.

Code
Java
6.

Complete Trie Implementation

The full implementation combining all basic operations.

What We Need:

  1. TrieNode class with children and isEnd
  2. insert(word) - add word to trie
  3. search(word) - check if exact word exists
  4. startsWith(prefix) - check if any word has prefix

LeetCode 208 - Implement Trie (Prefix Tree)

This is the foundation for all Trie problems. Master this implementation!

Code
Java
7.

Autocomplete with Trie

Given a prefix, find all words that start with it. The killer app for Tries!

Algorithm:

  1. Navigate to the prefix node
  2. If prefix doesn't exist, return []
  3. DFS from prefix node, collecting all complete words

Visual Trace:

Trie with: ["app", "apple", "application", "ape", "bat"]
autocomplete("app"):
1. Navigate: root → a → p → p
Now at node after "app"
2. DFS from here:
- Current node has isEnd=true → add "app"
- Go to 'l' → 'e' → isEnd=true → add "apple"
- Go to 'l' → 'i' → ... → add "application"
3. Result: ["app", "apple", "application"]

Optimization: Store word at isEnd node Instead of rebuilding string during DFS, store the complete word.

Code
Java
8.

Search Suggestions System

LeetCode 1268: Return top 3 suggestions as user types each character.

Problem:

products = ["mobile", "mouse", "moneypot", "monitor", "mousepad"]
searchWord = "mouse"
After typing 'm': ["mobile", "moneypot", "monitor"]
After typing 'mo': ["mobile", "moneypot", "monitor"]
After typing 'mou': ["mouse", "mousepad"]
After typing 'mous': ["mouse", "mousepad"]
After typing 'mouse': ["mouse", "mousepad"]

Approach:

  1. Build Trie from products
  2. For each prefix of searchWord:
    • Navigate to prefix node
    • DFS to get up to 3 lexicographically smallest words

Key Insight: DFS in alphabetical order naturally gives lexicographic results.

Code
Java
9.

Wildcard Search - Design Add and Search Words

Support '.' wildcard that matches any single character.

LeetCode 211:

addWord("bad")
addWord("dad")
addWord("mad")
search("pad") → false
search("bad") → true
search(".ad") → true (matches bad, dad, mad)
search("b..") → true (matches bad)

Algorithm: When we hit '.', try ALL children recursively. If ANY path succeeds, return true.

Visual Trace for search("b.d"):

At root:
'b' → go to 'b' child
At 'b' node:
'.' → try ALL children (here: 'a')
At 'a' node:
'd' → go to 'd' child
'd' has isEnd=true → MATCH!
Return true
Code
Java
10.

Word Search II - Trie + Grid Backtracking

Find all dictionary words in a 2D character grid. The hardest Trie problem!

LeetCode 212:

board = [["o","a","a","n"],
["e","t","a","e"],
["i","h","k","r"],
["i","f","l","v"]]
words = ["oath","pea","eat","rain"]
Output: ["eat","oath"]

Why Trie?

  • Without Trie: For each word, search entire grid → O(words × m×n × 4^L)
  • With Trie: One DFS, match multiple words → O(m×n × 4^L)

Algorithm:

  1. Build Trie from all words (store word at end node)
  2. DFS from each cell, following Trie
  3. When node.word exists, found a word!
  4. Set node.word = null to avoid duplicates

Key Optimization: Prune branches - if current path isn't in Trie, stop.

Code
Java
11.

Replace Words with Prefix

LeetCode 648: Replace words with their root/prefix if it exists.

Problem:

dictionary = ["cat", "bat", "rat"]
sentence = "the cattle was rattled by the battery"
Output: "the cat was rat by the bat"
cattle → cat (root exists)
rattled → rat (root exists)
battery → bat (root exists)

Algorithm:

  1. Build Trie from dictionary roots
  2. For each word in sentence:
    • Traverse Trie character by character
    • If hit isEnd, return that prefix (shortest root)
    • If path ends, return original word

Key Insight: Stop at FIRST isEnd (shortest prefix wins).

Code
Java
12.

Trie vs HashMap - When to Use Which

Making the right choice for your problem.

Time Complexity Comparison:

OperationHashMapTrie
InsertO(L)O(L)
Search exactO(L)O(L)
Search prefixO(N×L)O(L)
AutocompleteO(N×L)O(L + results)
DeleteO(L)O(L)

L = word length, N = total words

Space Comparison:

10,000 words, avg length 8:
- HashMap: ~400 KB (strings + overhead)
- Trie (array): ~2-10 MB (many null pointers)
- Trie (hashmap): ~800 KB - 2 MB

Use Trie When:

  • Prefix operations needed (autocomplete, startsWith)
  • Pattern matching with wildcards
  • Finding longest common prefix
  • Word games (Boggle, Scrabble)
  • IP routing tables

Use HashMap When:

  • Only exact match needed
  • Memory is constrained
  • Words have few common prefixes
  • Simple dictionary operations
13.

Common Edge Cases

Master these to avoid interview pitfalls.

1. Empty String

trie.insert(""); // Should this be allowed?
trie.search(""); // Returns true if empty string inserted

2. Single Character Words

trie.insert("a");
trie.insert("ab");
trie.search("a"); // true - check isEnd!
trie.startsWith("a"); // true

3. Prefix is Also a Word

trie.insert("app");
trie.insert("apple");
// Both "app" and "apple" should be findable

4. Case Sensitivity

// Usually lowercase only, but clarify!
trie.insert("App"); // index = 'A' - 'a' = negative!
// Solution: convert to lowercase or use HashMap

5. Non-Alphabetic Characters

trie.insert("hello-world"); // '-' crashes array-based!
// Solution: Use HashMap children

6. Duplicate Insertions

trie.insert("cat");
trie.insert("cat"); // Just sets isEnd=true again (harmless)

7. Search After No Insert

Trie trie = new Trie();
trie.search("anything"); // Should return false, not NPE
14.

Optimization Techniques

Advanced techniques for better performance.

1. Compressed Trie (Radix Tree) Merge single-child chains into one edge.

Before: After:
a abc
| |
b xyz
|
c
|
...

2. Ternary Search Tree Each node has 3 children: less, equal, greater. Better memory than 26-way Trie.

3. Word Storage at Nodes

class TrieNode {
TrieNode[] children;
String word; // Store word instead of rebuilding
}

4. Pruning in Word Search II

// Remove word after finding to avoid re-finding
if (node.word != null) {
result.add(node.word);
node.word = null; // Prune!
}
// Remove empty branches (advanced)
if (isEmpty(node)) {
parent.children[c] = null;
}

5. Prefix Count

class TrieNode {
int prefixCount; // How many words pass through this node
}
void insert(String word) {
for (char c : word) {
node.prefixCount++;
// ...
}
}
15.

Pattern Recognition Guide

Quick reference for choosing the right approach:

Problem TypeKey SignalApproach
Implement Trie"prefix tree"Basic insert/search/startsWith
Autocomplete"suggestions", "typeahead"Navigate to prefix, DFS
Wildcard search"." matches anyDFS trying all children
Word Search II"find words in grid"Trie + Grid DFS
Replace words"shortest prefix"Trie, stop at first isEnd
Longest word"can be built one char at a time"Trie with BFS/DFS
Palindrome pairs"concatenation forms palindrome"Trie with reverse words

Decision Tree:

Need prefix operations?
├── YES → Use Trie
│ ├── Autocomplete → DFS from prefix node
│ ├── Wildcards → DFS trying all children on '.'
│ └── Grid search → Trie + Backtracking
└── NO → Consider HashMap instead
Memory constrained?
├── YES → Use HashMap children or HashMap
└── NO → Array children (faster access)
16.

Complexity Analysis

Time Complexity:

  • Insert: O(L) where L = word length
  • Search: O(L)
  • StartsWith: O(L)
  • Autocomplete: O(L + total characters in results)
  • Wildcard with k dots: O(26^k × L) worst case
  • Word Search II: O(M × N × 4^L) where M×N = grid, L = max word length

Space Complexity:

Array-based (26 children):

  • O(N × L × 26) worst case
  • Each node: 26 pointers + boolean ≈ 208 bytes

HashMap-based:

  • O(N × L × avg_branching) average case
  • More memory efficient for sparse trees

Comparison for 10,000 words:

Structure Memory
--------------------------
HashSet<String> ~400 KB
Trie (HashMap) ~800 KB - 2 MB
Trie (Array) ~2 - 10 MB

Trade-off: Trie uses more memory but provides O(L) prefix operations that HashMap cannot match.

17.

Template Summary

Copy-paste ready templates.

Template 1: Basic Trie

class Trie {
TrieNode root = new TrieNode();
void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
int i = c - 'a';
if (node.children[i] == null)
node.children[i] = new TrieNode();
node = node.children[i];
}
node.isEnd = true;
}
boolean search(String word) {
TrieNode node = find(word);
return node != null && node.isEnd;
}
boolean startsWith(String prefix) {
return find(prefix) != null;
}
TrieNode find(String s) {
TrieNode node = root;
for (char c : s.toCharArray()) {
int i = c - 'a';
if (node.children[i] == null) return null;
node = node.children[i];
}
return node;
}
}

Template 2: Wildcard Search

boolean search(String word, int idx, TrieNode node) {
if (node == null) return false;
if (idx == word.length()) return node.isEnd;
char c = word.charAt(idx);
if (c == '.') {
for (TrieNode child : node.children)
if (search(word, idx + 1, child)) return true;
return false;
}
return search(word, idx + 1, node.children[c - 'a']);
}

Template 3: Word Search II

void dfs(char[][] board, int i, int j, TrieNode node, List<String> res) {
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length) return;
char c = board[i][j];
if (c == '#' || node.children[c-'a'] == null) return;
node = node.children[c-'a'];
if (node.word != null) { res.add(node.word); node.word = null; }
board[i][j] = '#';
dfs(board, i+1, j, node, res); dfs(board, i-1, j, node, res);
dfs(board, i, j+1, node, res); dfs(board, i, j-1, node, res);
board[i][j] = c;
}

Test Your Knowledge

Take a quick 15question quiz to test what you've learned.