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