Trie
What Is a Trie?
A Trie (also called a prefix tree or digital tree) is a tree where each node represents a character in a string. A path from the root to any node spells out a prefix. A path ending at a node marked as isEndOfWord = true spells a complete stored word.
Trie storing: ["cat", "car", "card", "care", "bat", "ball"]
(root)
/ \
c b
| |
a a
/ \ / \
t r t l
* | * |
/ \ l
d e *
* *
* = isEndOfWord = true (complete word ends here)
PATHS:
root → c → a → t* = "cat" ✓
root → c → a → r → d* = "card" ✓
root → c → a → r → e* = "care" ✓
root → c → a → r = "car"* ← BUT is "car" stored? Not in this example.
root → b → a → t* = "bat" ✓
root → b → a → l → l* = "ball" ✓
SHARED PREFIXES:
"cat" and "car" and "card" and "care" share c → a
Stored ONCE — that's why tries are space-efficient for related words.
Trie Node Structure
TRIE NODE:
┌─────────────────────────────────────────────┐
│ children: Node[26] or Map<char, Node> │
│ children['a'-'a'] = pointer to 'a' child │
│ children['b'-'a'] = pointer to 'b' child │
│ ... │
│ null if no child for that character │
│ │
│ isEndOfWord: boolean │
│ true = a complete word ends at this node │
│ false = only a prefix passes through here │
└─────────────────────────────────────────────┘
COMPARISON: array[26] vs HashMap
array[26]: HashMap<char, Node>:
✓ O(1) exact access ✓ Space proportional to actual children
✓ Simple implementation ✓ Handles any character set (Unicode, digits)
✗ 26 slots per node ✗ Slightly higher constant for hash lookup
(even if only 1 used)
Use array[26] for: lowercase a-z only (standard interview setting)
Use HashMap for: case-sensitive, mixed types, large alphabets
Complete Trie Implementation
1import java.util.*;
2
3public class Trie {
4
5 private static class TrieNode {
6 TrieNode[] children = new TrieNode[26];
7 boolean isEndOfWord = false;
8 }
9
10 private final TrieNode root = new TrieNode();
11
12 // INSERT word — O(L) where L = word length
13 public void insert(String word) {
14 TrieNode curr = root;
15 for (char c : word.toCharArray()) {
16 int idx = c - 'a';
17 if (curr.children[idx] == null) {
18 curr.children[idx] = new TrieNode(); // Create node if missing
19 }
20 curr = curr.children[idx];
21 }
22 curr.isEndOfWord = true; // Mark end of word
23 }
24
25 // SEARCH — returns true only if exact word is stored — O(L)
26 public boolean search(String word) {
27 TrieNode node = getNode(word);
28 return node != null && node.isEndOfWord;
29 }
30
31 // STARTS WITH — returns true if any stored word has this prefix — O(L)
32 public boolean startsWith(String prefix) {
33 return getNode(prefix) != null; // Path exists (don't need isEndOfWord)
34 }
35
36 // Helper: traverse to the node at end of string (or null if path missing)
37 private TrieNode getNode(String s) {
38 TrieNode curr = root;
39 for (char c : s.toCharArray()) {
40 int idx = c - 'a';
41 if (curr.children[idx] == null) return null; // Path broken
42 curr = curr.children[idx];
43 }
44 return curr;
45 }
46
47 // DELETE word — O(L)
48 public boolean delete(String word) {
49 return deleteHelper(root, word, 0);
50 }
51
52 private boolean deleteHelper(TrieNode node, String word, int depth) {
53 if (node == null) return false; // Word not found
54
55 if (depth == word.length()) {
56 if (!node.isEndOfWord) return false; // Word not in trie
57 node.isEndOfWord = false;
58 return !hasChildren(node); // Can this node be deleted?
59 }
60
61 int idx = word.charAt(depth) - 'a';
62 boolean shouldDelete = deleteHelper(node.children[idx], word, depth + 1);
63
64 if (shouldDelete) {
65 node.children[idx] = null; // Remove reference
66 return !node.isEndOfWord && !hasChildren(node);
67 }
68
69 return false;
70 }
71
72 private boolean hasChildren(TrieNode node) {
73 for (TrieNode child : node.children)
74 if (child != null) return true;
75 return false;
76 }
77
78 // COUNT words with given prefix — O(subtree size)
79 public int countWordsWithPrefix(String prefix) {
80 TrieNode node = getNode(prefix);
81 if (node == null) return 0;
82 return countWords(node);
83 }
84
85 private int countWords(TrieNode node) {
86 int count = node.isEndOfWord ? 1 : 0;
87 for (TrieNode child : node.children)
88 if (child != null) count += countWords(child);
89 return count;
90 }
91
92 public static void main(String[] args) {
93 Trie trie = new Trie();
94 trie.insert("cat"); trie.insert("car"); trie.insert("card");
95 trie.insert("care"); trie.insert("bat"); trie.insert("ball");
96
97 System.out.println(trie.search("cat")); // true
98 System.out.println(trie.search("car")); // false (not inserted!)
99 System.out.println(trie.search("card")); // true
100 System.out.println(trie.startsWith("ca")); // true
101 System.out.println(trie.startsWith("xyz")); // false
102
103 System.out.println(trie.countWordsWithPrefix("ca")); // 3 (cat, card, care)
104 System.out.println(trie.countWordsWithPrefix("ba")); // 2 (bat, ball)
105
106 trie.delete("card");
107 System.out.println(trie.search("card")); // false
108 System.out.println(trie.search("care")); // true (care still exists)
109 }
110}Output:
true
false
true
true
false
3 (cat, card, care)
2 (bat, ball)
false (card deleted)
true (care still exists)
Operations Traced
Insert "care" into existing trie with "cat", "car", "card"
Existing paths: root→c→a→t*, root→c→a→r→d*
Insert "care":
c: root.children['c'-'a'] exists → follow
a: c.children['a'-'a'] exists → follow
r: a.children['r'-'a'] exists → follow
e: r.children['e'-'a'] is null → CREATE new node
At new 'e' node: set isEndOfWord = true
Before: After:
root→c→a→t* root→c→a→t*
r→d* r→d*
e* ← new
Search "car" (not inserted as a word)
Traverse: root→c→a→r
All characters found — path exists ✓
BUT node 'r' has isEndOfWord = false
→ return false ("car" prefix exists but is not a word)
Search "cab":
root→c→a found, then 'b'
a.children['b'-'a'] = null → path breaks
→ return false
Application 1: Autocomplete
Given a prefix, return all words in the trie that start with it.
Trie: ["cat","car","card","care","bat","ball"]
Autocomplete("ca") → ["cat","card","care"]
Autocomplete("ba") → ["bat","ball"]
Autocomplete("xyz") → []
ALGORITHM:
1. Navigate to the prefix node (same as startsWith)
2. DFS the subtree — collect every path that reaches isEndOfWord=true
1import java.util.*;
2
3public class Autocomplete {
4
5 static class TrieNode {
6 TrieNode[] children = new TrieNode[26];
7 boolean isEnd = false;
8 }
9
10 static class AutocompleteTrie {
11 TrieNode root = new TrieNode();
12
13 void insert(String word) {
14 TrieNode curr = root;
15 for (char c : word.toCharArray()) {
16 int i = c - 'a';
17 if (curr.children[i] == null) curr.children[i] = new TrieNode();
18 curr = curr.children[i];
19 }
20 curr.isEnd = true;
21 }
22
23 List<String> autocomplete(String prefix) {
24 List<String> results = new ArrayList<>();
25 TrieNode curr = root;
26
27 // Navigate to prefix end
28 for (char c : prefix.toCharArray()) {
29 int i = c - 'a';
30 if (curr.children[i] == null) return results; // Prefix not found
31 curr = curr.children[i];
32 }
33
34 // DFS from prefix node — collect all complete words
35 collectWords(curr, new StringBuilder(prefix), results);
36 return results;
37 }
38
39 private void collectWords(TrieNode node, StringBuilder path, List<String> results) {
40 if (node.isEnd) results.add(path.toString());
41
42 for (int i = 0; i < 26; i++) {
43 if (node.children[i] != null) {
44 path.append((char)('a' + i));
45 collectWords(node.children[i], path, results);
46 path.deleteCharAt(path.length() - 1); // Backtrack
47 }
48 }
49 }
50 }
51
52 public static void main(String[] args) {
53 AutocompleteTrie trie = new AutocompleteTrie();
54 for (String w : new String[]{"cat","car","card","care","bat","ball"})
55 trie.insert(w);
56
57 System.out.println(trie.autocomplete("ca")); // [cat, card, care]
58 System.out.println(trie.autocomplete("ba")); // [ball, bat]
59 System.out.println(trie.autocomplete("car")); // [card, care]
60 System.out.println(trie.autocomplete("xyz")); // []
61 }
62}Output:
["cat", "card", "care"]
["ball", "bat"]
["card", "care"]
[]
Application 2: Wildcard Search (Word Dictionary)
Support '.' which matches any single character. DFS through all children at wildcard positions.
Dictionary: ["bad","dad","mad"]
search("pad") → false (no word starting with 'p')
search("bad") → true
search(".ad") → true ('.' matches 'b','d','m')
search("b..") → true ('b' then any two chars = 'bad')
ALGORITHM:
Regular character → follow single child
'.' → recurse into ALL non-null children
1public class WordDictionary {
2
3 private static class TrieNode {
4 TrieNode[] children = new TrieNode[26];
5 boolean isEnd = false;
6 }
7
8 private final TrieNode root = new TrieNode();
9
10 public void addWord(String word) {
11 TrieNode curr = root;
12 for (char c : word.toCharArray()) {
13 int i = c - 'a';
14 if (curr.children[i] == null) curr.children[i] = new TrieNode();
15 curr = curr.children[i];
16 }
17 curr.isEnd = true;
18 }
19
20 public boolean search(String word) {
21 return searchHelper(root, word, 0);
22 }
23
24 private boolean searchHelper(TrieNode node, String word, int idx) {
25 if (idx == word.length()) return node.isEnd;
26
27 char c = word.charAt(idx);
28
29 if (c != '.') {
30 // Regular character — follow single child
31 TrieNode next = node.children[c - 'a'];
32 return next != null && searchHelper(next, word, idx + 1);
33 } else {
34 // Wildcard — try ALL children
35 for (TrieNode child : node.children) {
36 if (child != null && searchHelper(child, word, idx + 1)) {
37 return true;
38 }
39 }
40 return false;
41 }
42 }
43
44 public static void main(String[] args) {
45 WordDictionary dict = new WordDictionary();
46 dict.addWord("bad"); dict.addWord("dad"); dict.addWord("mad");
47
48 System.out.println(dict.search("pad")); // false
49 System.out.println(dict.search("bad")); // true
50 System.out.println(dict.search(".ad")); // true (b/d/m + ad)
51 System.out.println(dict.search("b..")); // true (b + a + d)
52 System.out.println(dict.search("...")); // true (any 3 chars = bad/dad/mad)
53 System.out.println(dict.search("....")); // false (no 4-letter words)
54 }
55}Output:
false
true
true
true
true
false
Application 3: Longest Common Prefix
Find the longest string that is a prefix of all input strings.
["flower","flow","flight"] → "fl"
["dog","racecar","car"] → ""
["interview","interact","internal"] → "inter"
TRIE APPROACH:
Insert all words.
Walk from root: continue only when node has EXACTLY ONE child
AND isEndOfWord = false.
Stop at any branch or word endpoint.
["flower","flow","flight"]:
f: root has only child 'f' → continue, prefix="f"
l: f has only child 'l' → continue, prefix="fl"
Branch at 'l': children 'o' (flower,flow) and 'i' (flight) → STOP
Longest common prefix = "fl" ✓
1public class LongestCommonPrefix {
2
3 static class TrieNode {
4 TrieNode[] children = new TrieNode[26];
5 boolean isEnd = false;
6 int passCount = 0; // How many words pass through this node
7 }
8
9 public static String longestCommonPrefix(String[] words) {
10 if (words == null || words.length == 0) return "";
11
12 // Build trie
13 TrieNode root = new TrieNode();
14 for (String word : words) {
15 TrieNode curr = root;
16 for (char c : word.toCharArray()) {
17 int i = c - 'a';
18 if (curr.children[i] == null) curr.children[i] = new TrieNode();
19 curr = curr.children[i];
20 curr.passCount++;
21 }
22 curr.isEnd = true;
23 }
24
25 // Walk root until branch or end
26 StringBuilder prefix = new StringBuilder();
27 TrieNode curr = root;
28 int n = words.length;
29
30 while (true) {
31 TrieNode next = null;
32 int childCount = 0;
33
34 for (TrieNode child : curr.children) {
35 if (child != null && child.passCount == n) {
36 next = child;
37 childCount++;
38 }
39 }
40
41 // Stop if more than one path continues OR no path continues OR word ends here
42 if (childCount != 1 || curr.isEnd) break;
43
44 // Find which letter leads to next
45 for (int i = 0; i < 26; i++) {
46 if (curr.children[i] == next) {
47 prefix.append((char)('a' + i));
48 break;
49 }
50 }
51 curr = next;
52 }
53
54 return prefix.toString();
55 }
56
57 public static void main(String[] args) {
58 System.out.println(longestCommonPrefix(
59 new String[]{"flower","flow","flight"})); // fl
60 System.out.println(longestCommonPrefix(
61 new String[]{"dog","racecar","car"})); // (empty)
62 System.out.println(longestCommonPrefix(
63 new String[]{"interview","interact","internal"})); // inter
64 }
65}Output:
fl
(empty string)
inter
Trie vs HashMap vs Sorted Array
OPERATION HashMap<String,_> Sorted Array Trie
──────────────────────────────────────────────────────────────────
Exact search O(L) average O(L log n) O(L)
Insert O(L) average O(L + n) O(L)
Delete O(L) average O(L + n) O(L)
Prefix exists? O(L × n) worst O(L log n) O(L)
All words with prefix O(L × n) worst O(L log n + k) O(L + k)
Autocomplete O(L × n) O(L log n + k) O(L + k)
Sorted iteration O(n log n) O(n) O(n)
Space O(n × L) O(n × L) O(n × L) worst
Better w/ shared prefixes
k = number of matching words, n = number of stored words, L = query length
WHEN TO USE TRIE:
✓ Many prefix-based queries
✓ Autocomplete / type-ahead
✓ Finding all words with a given prefix
✓ Longest common prefix
✓ Wildcard matching
✓ Word break / word search problems
WHEN TO USE HASHMAP:
✓ Only exact lookups needed (no prefix queries)
✓ Simpler implementation needed
✓ Dynamic word set with no prefix queries
Complexity Summary
| Operation | Time | Space | Notes |
|---|---|---|---|
| Insert word | O(L) | O(L) new nodes | L = word length |
| Search exact | O(L) | O(1) | Follow path + check isEnd |
| Starts with | O(L) | O(1) | Follow path only |
| Delete word | O(L) | O(1) | Unmark isEnd + clean orphan nodes |
| Autocomplete | O(L + k) | O(k) | k = matching words × avg length |
| Wildcard search | O(26^w × L) | O(L) | w = wildcard count in pattern |
| Build trie (n words) | O(n × L) | O(n × L) | L = average word length |
| Longest common prefix | O(n × L) | O(n × L) | Build trie then walk |
Common Mistakes
Forgetting to mark isEndOfWord on insert. Without this flag, search("cat") returns false even after inserting "cat" — the path exists but no word is marked as complete. Every insert must set isEndOfWord = true on the final node.
Confusing search and startsWith. search("ca") returns false for a trie containing "cat" — "ca" itself is not stored as a word. startsWith("ca") returns true because the path c→a exists. The difference is whether you check isEndOfWord at the end.
Wildcard: returning true as soon as one child exists instead of recursing into all. At a '.', you must recurse into every non-null child and return true only if at least one subtree matches. Short-circuiting after checking one child misses valid matches in other children.
Deleting a word that is a prefix of another. Deleting "car" when "card" is also stored must only unset isEndOfWord at the 'r' node — it must NOT delete the 'r' node itself (the path to 'card' still needs it). Always check whether a node has children before physically removing it.
Using char - 'a' on uppercase or non-letter characters. The fixed array[26] approach only handles lowercase a-z. For uppercase, digits, or Unicode, switch to a HashMap<Character, TrieNode> for the children.
Interview Questions
Q: What makes a Trie more efficient than a HashMap for prefix queries?
A HashMap can check if an exact key exists in O(L), but to find all words with a given prefix you must scan all n stored keys — O(n × L). A Trie stores characters as tree edges: navigating the prefix takes O(L), and all words with that prefix live in a subtree rooted at the prefix node. Collecting them takes O(k) where k is the count of matching words. For large word sets with many prefix queries, the Trie reduces prefix queries from O(n × L) to O(L + k).
Q: How does wildcard search in a Trie differ from exact search?
Exact search follows a single predetermined path — one node per character, O(L). Wildcard search with '.' must explore all 26 possible paths at each wildcard position — it's a DFS where '.' causes branching. The worst case is O(26^w × L) where w is the number of wildcards. In practice, most branches fail early, making it faster than the worst case. The key implementation change: at '.', loop over all non-null children and recurse into each.
Q: When should you use a HashMap for children instead of an array[26]?
Use array[26] when the character set is lowercase a-z only — it gives O(1) access with a simple index formula and is standard for interview implementations. Use HashMap when: (1) the character set is larger (uppercase + lowercase + digits + special = 62+ characters); (2) the trie is sparse and memory is critical (HashMap only allocates space for actual children); (3) the input can contain Unicode. In competitive programming and interviews, array[26] is almost always sufficient and simpler.
Summary
A Trie is a tree where each edge represents one character. A path from root to a node spells a prefix; nodes marked isEndOfWord spell complete stored words.
Three core operations — all O(L):
- ›Insert — follow or create one node per character; mark
isEndOfWordat the last node - ›Search — follow the path; return
node != null && node.isEndOfWord - ›startsWith — follow the path; return
node != null(no isEndOfWord check)
Node structure: array[26] for lowercase a-z (constant space, O(1) access); HashMap for larger alphabets.
Four classic applications:
- ›Autocomplete — navigate to prefix node, DFS subtree collecting all words
- ›Wildcard search — at '.', recurse into all non-null children
- ›Longest common prefix — walk root until a branch or word endpoint
- ›Word break / word search — use trie to check prefixes during DFS
When Trie wins: any query involving prefixes — autocomplete, type-ahead, all-words-with-prefix, longest-common-prefix. O(L + k) vs O(n × L) for HashMap.
In the next topic you will explore Segment Tree — the range query data structure supporting O(log n) range sum, range minimum, and point updates.
What is the time complexity of inserting a word of length L into a Trie?