DSA Tries
A Trie (pronounced "try," from the word "retrieval") is a specialized tree-like data structure used to store strings or sequences. Unlike a regular binary tree, each node in a trie represents a single character, and strings are formed by following paths from the root to a leaf.
Tries are exceptionally fast for operations involving strings — especially prefix-based searching — making them the data structure behind features like autocomplete, spell checkers, and IP routing tables.
How a Trie Works
Consider inserting the words "cat", "car", and "cart" into a trie:
# Trie structure after inserting "cat", "car", "cart":
#
# root
# └── c
# └── a
# ├── t (end of "cat")
# └── r (end of "car")
# └── t (end of "cart")
#
# The prefix "ca" is shared by all three words — no duplication
Shared prefixes are stored only once, making a trie memory-efficient for large vocabularies with common prefixes.
Implementing a Trie in Python
Step 1: Define the TrieNode
class TrieNode:
def __init__(self):
self.children = {} # Dictionary: character -> TrieNode
self.is_end_of_word = False # True if this node ends a complete word
Step 2: Define the Trie Class with Insert
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode() # Create new node if missing
node = node.children[char] # Move to the next node
node.is_end_of_word = True # Mark end of word
trie = Trie()
trie.insert("cat")
trie.insert("car")
trie.insert("cart")
trie.insert("card")
trie.insert("dog")
Step 3: Search for a Word — O(m), where m = word length
def search(self, word):
node = self.root
for char in word:
if char not in node.children:
return False # Character not found — word doesn't exist
node = node.children[char]
return node.is_end_of_word # True only if this is a complete word
print(trie.search("car")) # Output: True
print(trie.search("ca")) # Output: False (not a complete word)
print(trie.search("dog")) # Output: True
print(trie.search("dot")) # Output: False
Step 4: Check if a Prefix Exists — O(m)
def starts_with(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True # All prefix characters found
print(trie.starts_with("ca")) # Output: True
print(trie.starts_with("do")) # Output: True
print(trie.starts_with("xyz")) # Output: False
Complete Trie with All Operations
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word):
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
def starts_with(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
# Build and query the trie
t = Trie()
for word in ["apple", "app", "application", "apt", "bat", "ball"]:
t.insert(word)
print(t.search("app")) # True
print(t.search("apply")) # False
print(t.starts_with("app")) # True
print(t.starts_with("ba")) # True
print(t.starts_with("xyz")) # False
Autocomplete Feature Using a Trie
Finding all words with a given prefix is one of the most common real-world uses of a trie.
def autocomplete(trie, prefix):
node = trie.root
for char in prefix:
if char not in node.children:
return [] # Prefix not found
node = node.children[char]
results = []
# DFS from the node reached by the prefix
def dfs(current_node, current_word):
if current_node.is_end_of_word:
results.append(current_word)
for char, child in current_node.children.items():
dfs(child, current_word + char)
dfs(node, prefix)
return results
t = Trie()
for word in ["apple", "app", "application", "apt", "apt", "bat", "ball", "banana"]:
t.insert(word)
print(autocomplete(t, "app")) # Output: ['app', 'apple', 'application']
print(autocomplete(t, "ba")) # Output: ['bat', 'ball', 'banana']
Deleting a Word from a Trie
def delete(root, word, depth=0):
if root is None:
return None
if depth == len(word):
if root.is_end_of_word:
root.is_end_of_word = False
if not root.children:
return None # Node can be deleted
return root
char = word[depth]
if char in root.children:
root.children[char] = delete(root.children[char], word, depth + 1)
if root.children[char] is None:
del root.children[char]
return root
Tries vs Hash Maps for Strings
- Prefix search: Tries are O(m); hash maps require full string matching for every entry — O(n × m).
- Sorted iteration: Tries support lexicographic traversal naturally; hash maps do not maintain order.
- Memory: Tries use more memory per character; hash maps use less with no prefix sharing benefit.
- Exact lookup: Hash maps are faster for exact key lookup — O(1).
Big O Summary for Trie Operations
- Insert — O(m), where m is the length of the word
- Search — O(m)
- Prefix check (starts_with) — O(m)
- Autocomplete (all words with prefix) — O(m + k), where k is total characters in results
- Space — O(n × m) where n is the number of words
Key Points
- A trie stores strings by sharing common prefixes across nodes, one character per node.
- Each node holds a dictionary of children and a flag indicating whether it marks the end of a word.
- Insert, search, and prefix check all run in O(m) time — proportional to the word length, not the dictionary size.
- Tries are ideal for autocomplete, spell checking, IP routing, and dictionary applications.
- Unlike hash maps, tries support prefix-based operations natively and efficiently.
- The DFS technique is used to collect all words sharing a given prefix for autocomplete features.
