Tries (Prefix Trees) Made Easy

Master Tries step-by-step with visual explanations and real-world analogies

🌳 Tree Structure ⏱️ 25 min read 🔤 String Search 🔄 Autocomplete

🤔 Why Should You Care About Tries?

🔍

Google Search Autocomplete

When you type in Google, it instantly suggests completions. That's a Trie working behind the scenes, finding all words with your prefix!

📱

Phone Keyboard Predictions

Your phone's keyboard predicts the next word you'll type using Trie structures to quickly find matching words.

✏️

Spell Checkers

Word processors use Tries to quickly check if your word exists in the dictionary and suggest corrections.

🎮

Word Games

Games like Scrabble and Wordle use Tries to validate words and find all possible word combinations.

🌐

IP Routing Tables

Internet routers use Tries to match IP addresses and determine the best path for data packets.

💾

Database Indexing

Databases use Trie-like structures for prefix searching in string columns, making queries lightning fast.

🌟 Level 1: The Basics (Start Here!)

Level 1

What is a Trie? Think of it as a Smart Dictionary! 📚

Imagine organizing words letter by letter, where common prefixes share the same path. It's like a phone book where "CAT", "CATS", and "CAR" all start with "CA"!

🌳 Building a Simple Trie

Let's add words: "cat", "cats", "car", "card", "dog"

root c d a o t* r* g* s* d* * = end of word

Words stored: cat, cats, car, card, dog 🎉

Key Properties of a Trie 🔑

🎯 Prefix Sharing

Words with common prefixes share the same path.

Example: "cat" and "car" both share "ca"

⚡ Fast Operations

Search, insert, delete all take O(m) time where m = word length.

No matter how many words!

Your First Trie Implementation 🌳
# Simple Trie Node class TrieNode: def __init__(self): self.children = {} # Dictionary to store child nodes self.is_end_word = False # Is this the end of a word? # The Trie itself class Trie: def __init__(self): self.root = TrieNode() # Start with empty root def insert(self, word): """Add a word to the Trie""" node = self.root # Travel down the tree, creating nodes as needed for char in word: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] # Mark the end of the word node.is_end_word = True print(f"✅ Added '{word}' to the Trie!") def search(self, word): """Check if a word exists in the Trie""" node = self.root for char in word: if char not in node.children: return False # Character not found node = node.children[char] return node.is_end_word # Must be a complete word # Try it out! trie = Trie() trie.insert("cat") trie.insert("car") print(trie.search("cat")) # True ✓ print(trie.search("ca")) # False (not a complete word)

⚠️ Common Mistake #1: Forgetting to Mark Word Endings

# ❌ WRONG - Doesn't mark end of word! def insert_wrong(self, word): node = self.root for char in word: if char not in node.children: node.children[char] = TrieNode() node = node.children[char] # Forgot: node.is_end_word = True

✅ The Right Way

# ✅ CORRECT - Always mark the end! def insert_correct(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_word = True # Don't forget this!

🎮 Try It Yourself: Build Your Own Trie

Add words to the Trie and see the structure grow!

Words in Trie: None yet

🎨 Level 2: Common Trie Patterns

Level 2

Pattern 1: Autocomplete System 🔍

Build an autocomplete feature like Google Search!

How Autocomplete Works

1
User types: Navigate to prefix in Trie
2
Find all words: DFS from that node
3
Return suggestions: Collect all complete words
Autocomplete Implementation
def autocomplete(self, prefix): """Find all words that start with prefix""" results = [] node = self.root # Step 1: Navigate to the prefix end for char in prefix: if char not in node.children: return results # Prefix doesn't exist node = node.children[char] # Step 2: Find all words from this point def dfs(current_node, current_word): if current_node.is_end_word: results.append(current_word) for char, child_node in current_node.children.items(): dfs(child_node, current_word + char) # Include the prefix itself if it's a word if node.is_end_word: results.append(prefix) # Find all continuations for char, child_node in node.children.items(): dfs(child_node, prefix + char) return results # Example usage trie = Trie() words = ["apple", "app", "apricot", "application"] for word in words: trie.insert(word) suggestions = trie.autocomplete("app") print(suggestions) # ['app', 'apple', 'application']

🔮 Live Autocomplete Demo

Type to see autocomplete in action!

Pattern 2: Word Search in a Grid (Boggle) 🎮

Word Search with Trie
def find_words_in_grid(board, word_list): """Find all words from word_list in the grid""" # Build Trie from word list trie = Trie() for word in word_list: trie.insert(word) rows, cols = len(board), len(board[0]) found_words = set() def dfs(i, j, node, path): # Check if we found a word if node.is_end_word: found_words.add(path) # Check bounds and if character exists in Trie if i < 0 or i >= rows or j < 0 or j >= cols: return char = board[i][j] if char not in node.children: return # Mark as visited and explore neighbors board[i][j] = '#' for di, dj in [(0,1), (1,0), (0,-1), (-1,0)]: dfs(i + di, j + dj, node.children[char], path + char) # Restore the cell board[i][j] = char # Start DFS from each cell for i in range(rows): for j in range(cols): dfs(i, j, trie.root, "") return list(found_words)

Pattern 3: Longest Common Prefix 🔤

Finding Longest Common Prefix
def longest_common_prefix(self): """Find the longest prefix shared by all words""" node = self.root prefix = [] # Keep going while there's only one path while node and len(node.children) == 1 and not node.is_end_word: char = list(node.children.keys())[0] prefix.append(char) node = node.children[char] return ''.join(prefix) # Example trie = Trie() words = ["flower", "flow", "flight"] for word in words: trie.insert(word) print(trie.longest_common_prefix()) # "fl"

💪 Level 3: Practice Problems

Practice Time!

Problem Set: From Easy to Hard

E

Easy: Implement startsWith() 🎯

Check if any word in the Trie starts with a given prefix.

💡 Hint

Navigate to the prefix. If you can reach it, return True!

M

Medium: Replace Words 📝

Given a dictionary and a sentence, replace words with their shortest root.

💡 Hint

For each word, find the shortest prefix that exists in the Trie.

H

Hard: Design Search Autocomplete System 🔥

Design a system that returns top 3 hot sentences based on frequency.

💡 Hint

Store frequency at end nodes. Use heap for top 3.

🎯 Challenge: Implement startsWith()

Complete the function to check if any word starts with the given prefix.

def starts_with(self, prefix): """Return True if any word starts with prefix""" # Your code here! # Hint: Similar to search, but don't check is_end_word pass
Show Solution
def starts_with(self, prefix): """Return True if any word starts with prefix""" node = self.root # Navigate to the end of prefix for char in prefix: if char not in node.children: return False node = node.children[char] # If we reached here, prefix exists! return True

🚀 Level 4: Advanced Techniques

Level 4

Compressed Trie (Radix Tree) 🗜️

Save space by compressing chains of single children!

Regular Trie vs Compressed Trie

Regular Trie

t → e → s → t

4 nodes for "test"

Compressed

test

1 node for "test"!

Compressed Trie Implementation
class CompressedTrieNode: def __init__(self): self.children = {} self.is_end_word = False self.edge_label = "" # Store edge string def compress_trie(root): """Compress chains of single children""" def compress_node(node): # If only one child and not end of word if len(node.children) == 1 and not node.is_end_word: child_char = list(node.children.keys())[0] child_node = node.children[child_char] # Merge with child node.edge_label += child_char node.children = child_node.children node.is_end_word = child_node.is_end_word # Continue compression compress_node(node) else: # Recursively compress children for child in node.children.values(): compress_node(child) compress_node(root) return root

Trie with Delete Operation 🗑️

Deleting Words from Trie
def delete(self, word): """Delete a word from the Trie""" def delete_helper(node, word, index): if index == len(word): # Reached end of word if not node.is_end_word: return False # Word doesn't exist node.is_end_word = False # Return True if node has no children return len(node.children) == 0 char = word[index] if char not in node.children: return False should_delete = delete_helper(node.children[char], word, index + 1) if should_delete: del node.children[char] # Return True if current node has no children and is not end of another word return len(node.children) == 0 and not node.is_end_word return False delete_helper(self.root, word, 0)

Bit Trie for XOR Problems 🔢

Use Tries to solve bit manipulation problems!

Maximum XOR with Bit Trie
def find_maximum_xor(nums): """Find maximum XOR of any two numbers""" class BitTrieNode: def __init__(self): self.children = {} # 0 or 1 root = BitTrieNode() # Insert all numbers into Trie for num in nums: node = root for i in range(31, -1, -1): bit = (num >> i) & 1 if bit not in node.children: node.children[bit] = BitTrieNode() node = node.children[bit] max_xor = 0 # Find maximum XOR for each number for num in nums: node = root current_xor = 0 for i in range(31, -1, -1): bit = (num >> i) & 1 # Try to go opposite direction for maximum XOR toggle_bit = 1 - bit if toggle_bit in node.children: current_xor |= (1 << i) node = node.children[toggle_bit] else: node = node.children[bit] max_xor = max(max_xor, current_xor) return max_xor

📖 Quick Reference Guide

🎯 When to Use a Trie

  • ✅ Need fast prefix matching (autocomplete, spell check)
  • ✅ Multiple string searches with common prefixes
  • ✅ Word games and puzzles (Boggle, Scrabble)
  • ✅ IP routing and network protocols
  • ✅ Need to check if string is prefix of another

⏱️ Time Complexities

  • 🔍 Search: O(m) - m = word length
  • Insert: O(m)
  • 🗑️ Delete: O(m)
  • 🔤 Prefix Search: O(p) - p = prefix length
  • 📝 Autocomplete: O(p + n) - n = results

💾 Space Complexity

  • 📊 Worst Case: O(ALPHABET_SIZE × N × M)
  • 📊 N: Number of words
  • 📊 M: Average word length
  • 📊 Better with: Compressed Tries
  • 📊 Alternative: Ternary Search Trees

🚀 Pro Tips for Trie Success

  1. Use dictionary: More efficient than array of 26 for sparse data
  2. Mark word endings: Essential for distinguishing prefixes from words
  3. Consider compression: For memory-constrained environments
  4. Cache common searches: Store frequent autocomplete results
  5. Clean up unused nodes: Implement delete to free memory
  6. Use for prefixes: Tries excel at prefix operations

🔄 Trie vs Other Data Structures

Operation Trie Hash Table BST
Search O(m) O(1) avg O(m log n)
Prefix Search O(p) ✅ O(n) O(m log n)
Space High Medium Low