DSA Tutorial
🔍

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

Java
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
Java
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
Java
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" ✓
Java
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

OperationTimeSpaceNotes
Insert wordO(L)O(L) new nodesL = word length
Search exactO(L)O(1)Follow path + check isEnd
Starts withO(L)O(1)Follow path only
Delete wordO(L)O(1)Unmark isEnd + clean orphan nodes
AutocompleteO(L + k)O(k)k = matching words × avg length
Wildcard searchO(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 prefixO(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 isEndOfWord at 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.

Suggested Quiz

What is the time complexity of inserting a word of length L into a Trie?

1/6
Trie