Trie (Prefix Tree)
A Trie (pronounced "try", from retrieval) is a tree-based data structure designed for storing and searching strings with shared prefixes. Unlike a HashMap, a Trie lets you search by prefix in O(m) time where m is the length of the word โ making it the backbone of search autocomplete, spell checkers, and IP routing.
๐ง Why Trie? โ Comparison with Other Structuresโ
Store words: ["apple", "app", "apt", "bat", "ball"]
Then answer: "Give all words starting with 'ap'"
โ Array/List:
Scan every word โ O(n ร m) per query โ slow for large dictionaries
โ HashMap:
O(1) exact lookup but NO prefix search support
"app" โ exists, but "give all words starting with ap" requires scanning all keys
โ
Trie:
Insert: O(m) per word
Prefix search: O(m) to reach prefix node, then collect all words below โ Fast!
๐ณ Trie Structure โ Visual Diagramโ
Words inserted: ["app", "apple", "apt", "bat", "ball"]
root
/ \
a b
| |
p a
/ \ / \
p t t l
| | | |
[โ] [โ][โ] l
| |
l [โ]
|
e
|
[โ]
[โ] = isEndOfWord is true
Paths from root:
rootโaโpโp = "app" โ
rootโaโpโpโlโe = "apple" โ
rootโaโpโt = "apt" โ
rootโbโaโt = "bat" โ
rootโbโaโlโl = "ball" โ
๐ฉ Node Structureโ
Each Trie node contains:
1. children[26] โ array of 26 pointers (one per lowercase letter a-z)
children[0] = pointer to 'a' child
children[1] = pointer to 'b' child
...
children[25] = pointer to 'z' child
2. isEndOfWord โ boolean flag: true if a valid word ends at this node
Initially all children = null, isEndOfWord = false
โ Operation 1: Insertโ
Visual Dry-Run โ Insert "app"โ
Start at root. Word = "app"
Character 'a':
root.children[0] == null? โ YES โ create new node
Move to children[0]
Character 'p':
node.children[15] == null? โ YES โ create new node
Move to children[15]
Character 'p':
node.children[15] == null? โ YES โ create new node
Move to children[15]
End of word โ set isEndOfWord = true โ
root โ [a] โ [p] โ [p*] (* = end of word)
Visual Dry-Run โ Insert "apple" after "app"โ
Word = "apple"
'a' โ node exists (root.children[0]) โ just move, no new node
'p' โ node exists โ just move
'p' โ node exists โ just move (this was end of "app", stays end)
'l' โ null โ create new node, move
'e' โ null โ create new node, move
End โ isEndOfWord = true โ
root โ [a] โ [p] โ [p*] โ [l] โ [e*]
Codeโ
- Python
- Java
- C++
class TrieNode:
def __init__(self):
self.children = {} # char โ TrieNode
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode() # create node if missing
node = node.children[char] # move to next node
node.is_end_of_word = True # mark end of word
# Example
trie = Trie()
trie.insert("app")
trie.insert("apple")
trie.insert("apt")
import java.util.HashMap;
class TrieNode {
HashMap<Character, TrieNode> children;
boolean isEndOfWord;
public TrieNode() {
children = new HashMap<>();
isEndOfWord = false;
}
}
public class Trie {
protected TrieNode root;
public Trie() {
root = new TrieNode();
}
public void insert(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
if (!node.children.containsKey(c)) {
node.children.put(c, new TrieNode()); // create node if missing
}
node = node.children.get(c); // move to next node
}
node.isEndOfWord = true; // mark end of word
}
}
#include <vector>
#include <string>
#include <iostream>
using namespace std;
struct TrieNode {
vector<TrieNode*> children;
bool isEndOfWord;
TrieNode() : children(26, nullptr), isEndOfWord(false) {}
~TrieNode() {
for (auto child : children) {
delete child;
}
}
};
class Trie {
public:
TrieNode* root;
Trie() {
root = new TrieNode();
}
~Trie() {
delete root;
}
void insert(const string& word) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (!node->children[idx]) {
node->children[idx] = new TrieNode(); // create node if missing
}
node = node->children[idx]; // move to next node
}
node->isEndOfWord = true; // mark end of word
}
};
Time Complexity: O(m) where m = length of word Space Complexity: O(m) per word inserted
๐ Operation 2: Search (Exact Word)โ
Visual Dry-Runโ
Trie contains: ["app", "apple", "apt"]
root โ [a] โ [p] โ [p*] โ [l] โ [e*]
โ [t*]
Search "app":
'a' โ exists โ move
'p' โ exists โ move
'p' โ exists โ move
End โ isEndOfWord = TRUE โ
โ "app" EXISTS
Search "ap":
'a' โ exists โ move
'p' โ exists โ move
End โ isEndOfWord = FALSE โ โ "ap" is NOT a word
(node exists as prefix but not marked as complete word)
Codeโ
- Python
- Java
- C++
def search(self, word: str) -> bool:
node = self.root
for char in word:
if char not in node.children:
return False # path doesn't exist
node = node.children[char]
return node.is_end_of_word # true only if complete word
public boolean search(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
if (!node.children.containsKey(c)) {
return false; // path doesn't exist
}
node = node.children.get(c);
}
return node.isEndOfWord; // true only if complete word
}
bool search(const string& word) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (!node->children[idx]) {
return false; // path doesn't exist
}
node = node->children[idx];
}
return node->isEndOfWord; // true only if complete word
}
Time Complexity: O(m) ย |ย Space Complexity: O(1)
๐ Operation 3: StartsWith (Prefix Search)โ
Visual Dry-Runโ
startsWith("ap"):
'a' โ exists โ move
'p' โ exists โ move
Reached end of prefix โ node exists โ TRUE โ
startsWith("bx"):
'b' โ exists โ move
'x' โ does NOT exist โ FALSE โ
Key difference:
search("ap") โ FALSE (not a complete word)
startsWith("ap") โ TRUE (it IS a valid prefix)
Codeโ
- Python
- Java
- C++
def starts_with(self, prefix: str) -> bool:
node = self.root
for char in prefix:
if char not in node.children:
return False # prefix path doesn't exist
node = node.children[char]
return True # prefix exists
public boolean startsWith(String prefix) {
TrieNode node = root;
for (char c : prefix.toCharArray()) {
if (!node.children.containsKey(c)) {
return false; // prefix path doesn't exist
}
node = node.children.get(c);
}
return true; // prefix exists
}
bool startsWith(const string& prefix) {
TrieNode* node = root;
for (char c : prefix) {
int idx = c - 'a';
if (!node->children[idx]) {
return false; // prefix path doesn't exist
}
node = node->children[idx];
}
return true; // prefix exists
}
Time Complexity: O(m) ย |ย Space Complexity: O(1)
๐๏ธ Operation 4: Deleteโ
Visual Dry-Runโ
Delete "app" from Trie containing ["app", "apple", "apt"]
Before:
root โ [a] โ [p] โ [p*] โ [l] โ [e*]
โ [t*]
Strategy: Only unmark isEndOfWord at the last node.
Do NOT delete the node โ "apple" still needs this path!
After deleting "app":
root โ [a] โ [p] โ [p] โ [l] โ [e*] โ 'p' no longer end of word
โ [t*]
search("app") โ FALSE โ
(deleted)
search("apple") โ TRUE โ
(intact)
Codeโ
- Python
- Java
- C++
def delete(self, word: str) -> bool:
if not self.search(word):
return False
def _delete(node, word, depth):
if depth == len(word):
node.is_end_of_word = False
return len(node.children) == 0
char = word[depth]
if _delete(node.children[char], word, depth + 1):
del node.children[char]
return len(node.children) == 0 and not node.is_end_of_word
return False
_delete(self.root, word, 0)
return True
public boolean delete(String word) {
if (!search(word)) return false;
deleteHelper(root, word, 0);
return true;
}
private boolean deleteHelper(TrieNode node, String word, int depth) {
if (depth == word.length()) {
node.isEndOfWord = false;
return node.children.isEmpty();
}
char c = word.charAt(depth);
boolean shouldDelete = deleteHelper(node.children.get(c), word, depth + 1);
if (shouldDelete) {
node.children.remove(c);
return node.children.isEmpty() && !node.isEndOfWord;
}
return false;
}
bool deleteWord(const string& word) {
if (!search(word)) return false;
deleteWordHelper(root, word, 0);
return true;
}
private:
bool deleteWordHelper(TrieNode* node, const string& word, int depth) {
if (depth == (int)word.size()) {
node->isEndOfWord = false;
for (auto child : node->children)
if (child) return false;
return true;
}
int idx = word[depth] - 'a';
bool shouldDelete = deleteWordHelper(node->children[idx], word, depth + 1);
if (shouldDelete) {
delete node->children[idx];
node->children[idx] = nullptr;
if (!node->isEndOfWord) {
for (auto child : node->children)
if (child) return false;
return true;
}
}
return false;
}
Time Complexity: O(m) ย |ย Space Complexity: O(m) recursion stack
โจ Bonus: AutoComplete Featureโ
Visual Dry-Runโ
Trie: ["app", "apple", "apt", "bat", "ball"]
AutoComplete("ap"):
Step 1: Navigate to node for prefix "ap"
root โ 'a' โ 'p' โ [at this node]
Step 2: DFS from this node collecting all complete words
โ 'p'* โ found "app"
โ 'l' โ 'e'* โ found "apple"
โ 't'* โ found "apt"
Result: ["app", "apple", "apt"] โ
Codeโ
- Python
- Java
- C++
class TrieWithAutoComplete(Trie):
def autocomplete(self, prefix: str) -> list:
node = self.root
# Navigate to prefix node
for char in prefix:
if char not in node.children:
return []
node = node.children[char]
# DFS to collect all words
results = []
self._dfs(node, prefix, results)
return results
def _dfs(self, node, current_word, results):
if node.is_end_of_word:
results.append(current_word)
for char, child_node in node.children.items():
self._dfs(child_node, current_word + char, results)
# Example
trie = TrieWithAutoComplete()
for word in ["app", "apple", "apt", "bat", "ball"]:
trie.insert(word)
print(trie.autocomplete("ap")) # ['app', 'apple', 'apt']
print(trie.autocomplete("ba")) # ['bat', 'ball']
print(trie.autocomplete("xyz")) # []
import java.util.ArrayList;
import java.util.List;
public class TrieWithAutoComplete extends Trie {
public List<String> autocomplete(String prefix) {
TrieNode node = root;
List<String> results = new ArrayList<>();
// Navigate to prefix node
for (char c : prefix.toCharArray()) {
if (!node.children.containsKey(c)) return results;
node = node.children.get(c);
}
// DFS to collect all words
dfs(node, new StringBuilder(prefix), results);
return results;
}
private void dfs(TrieNode node, StringBuilder current, List<String> results) {
if (node.isEndOfWord) {
results.add(current.toString());
}
for (char c : node.children.keySet()) {
current.append(c);
dfs(node.children.get(c), current, results);
current.deleteCharAt(current.length() - 1); // backtrack
}
}
}
class TrieWithAutoComplete : public Trie {
public:
vector<string> autocomplete(const string& prefix) {
TrieNode* node = root;
vector<string> results;
// Navigate to prefix node
for (char c : prefix) {
int idx = c - 'a';
if (!node->children[idx]) return results;
node = node->children[idx];
}
// DFS to collect all words
string current = prefix;
dfs(node, current, results);
return results;
}
private:
void dfs(TrieNode* node, string& current, vector<string>& results) {
if (node->isEndOfWord) results.push_back(current);
for (int i = 0; i < 26; i++) {
if (node->children[i]) {
current.push_back('a' + i);
dfs(node->children[i], current, results);
current.pop_back(); // backtrack
}
}
}
};
โก Brute Force vs Trieโ
Dictionary: 100,000 words, average length 8
Query: "Find all words starting with 'pre'"
โ BRUTE FORCE โ O(n ร m):
Check every word: 100,000 ร 8 = 800,000 operations per query
โ
TRIE โ O(m + k):
Navigate prefix "pre": 3 steps
Collect k=500 matching words: 503 operations total
Speedup: ~1,600x faster โ
๐ Complexity Summaryโ
| Operation | Time | Space |
|---|---|---|
| Insert | O(m) | O(m) |
| Search | O(m) | O(1) |
| StartsWith | O(m) | O(1) |
| Delete | O(m) | O(m) stack |
| AutoComplete | O(m + k) | O(k) |
m = length of word/prefix ย |ย k = number of results returned
โ Common Mistakesโ
- Confusing search() and startsWith() โ
search("app")checks for a complete word.startsWith("app")checks for any word with that prefix. They return different results! - Deleting shared prefix nodes โ Never delete a node if other words share its path. Always check children before removing.
- Case sensitivity โ Tries are case-sensitive. Always normalize input to lowercase before inserting or searching.
- Array vs HashMap for children โ Array
children[26]is O(1) access but wastes memory. HashMap uses less memory but has overhead. Choose based on your character set size. - Forgetting isEndOfWord โ Inserting "apple" without marking
isEndOfWord=truecausessearch("apple")to return false even though the path exists.
๐๏ธ Practice Problemsโ
| # | Problem | Concept | Difficulty |
|---|---|---|---|
| 1 | Implement Trie (Prefix Tree) | Insert, Search, StartsWith | ๐ก Medium |
| 2 | Longest Common Prefix | Trie traversal | ๐ข Easy |
| 3 | Design Add and Search Words | Wildcard search in Trie | ๐ก Medium |
| 4 | Replace Words | Prefix replacement | ๐ก Medium |
| 5 | Map Sum Pairs | Trie with values | ๐ก Medium |
| 6 | Word Search II | Trie + DFS on grid | ๐ด Hard |
| 7 | Maximum XOR of Two Numbers | Binary Trie | ๐ด Hard |
| 8 | Stream of Characters | Trie + sliding window | ๐ด Hard |
๐ Real-World Applicationsโ
- Search autocomplete โ Google and Bing suggest completions as you type
- Spell checker โ Word processors verify if a word exists in the dictionary
- IP routing (LPM) โ Routers use binary Tries for Longest Prefix Match
- T9 predictive text โ Mobile keyboard word prediction
- DNA sequence matching โ Biological sequence databases
๐ Referencesโ
Finished reading? Mark this topic as complete.