Skip to main content

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​

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")

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​

    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

Time Complexity: O(m) Β |Β  Space Complexity: O(1)


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​

    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

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​

    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

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​

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")) # []

⚑ 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​

OperationTimeSpace
InsertO(m)O(m)
SearchO(m)O(1)
StartsWithO(m)O(1)
DeleteO(m)O(m) stack
AutoCompleteO(m + k)O(k)

m = length of word/prefix Β |Β  k = number of results returned


❌ Common Mistakes​

  1. Confusing search() and startsWith() β€” search("app") checks for a complete word. startsWith("app") checks for any word with that prefix. They return different results!
  2. Deleting shared prefix nodes β€” Never delete a node if other words share its path. Always check children before removing.
  3. Case sensitivity β€” Tries are case-sensitive. Always normalize input to lowercase before inserting or searching.
  4. 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.
  5. Forgetting isEndOfWord β€” Inserting "apple" without marking isEndOfWord=true causes search("apple") to return false even though the path exists.

πŸ‹οΈ Practice Problems​

#ProblemConceptDifficulty
1Implement Trie (Prefix Tree)Insert, Search, StartsWith🟑 Medium
2Longest Common PrefixTrie traversal🟒 Easy
3Design Add and Search WordsWildcard search in Trie🟑 Medium
4Replace WordsPrefix replacement🟑 Medium
5Map Sum PairsTrie with values🟑 Medium
6Word Search IITrie + DFS on gridπŸ”΄ Hard
7Maximum XOR of Two NumbersBinary TrieπŸ”΄ Hard
8Stream of CharactersTrie + 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​