Trie (Prefix Tree)
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.
Video Explanationโ

๐ง 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
๐ Related Topicsโ
- Segment Tree: Tree structure specialized for range queries and updates ().
- Disjoint Set Union (DSU): Manages dynamic connectivity and equivalence relations ().
- Two Pointers & Sliding Window: Efficient subarray and substring processing techniques ().
๐ Referencesโ
Test Your Understanding
Answer these 3 questions to check what you have learned.
What is the main topic of this documentation page?
What should be identified before implementing trie?
How is understanding of trie best checked?
Done with this topic? Mark it as complete to track your progress.