A trie (pronounced "try") is like a word search puzzle, where each path from the start spells out a different word, letter by letter. Also known as a prefix tree, a trie is a tree that stores strings by breaking them down into individual characters. Unlike regular trees that store complete values at each node, tries build words character by character along paths, which makes them a natural fit for autocomplete and spell checking.
Tries let us add words by creating paths of letters, and find words by walking those paths one letter at a time. Since words with the same beginning share the same path, tries make it quick to look up a word or check whether it exists.
| Operations | Time | Space |
|---|---|---|
| Insert Word | O(m) | O(m) |
| Search Word | O(m) | O(1) |
| Search Prefix | O(m) | O(1) |
| Delete Word | O(m) | O(1) |
| Count Words | O(1) | O(1) |
| Get All Words | O(n) | O(n) |
The efficiency of a trie depends on the alphabet size (often written as Σ), which is how many different characters we might use. If you're only storing lowercase English words, the alphabet size is 26. This matters because each node has to be ready to handle any character that might come next.
A trie node is like a single letter in a word puzzle: each node represents one character. Instead of storing complete words, each node uses a hashmap to track which letters can come next. That means words starting with the same letters share the same path. For example, car and cat would share the nodes for c and a before splitting into different paths.
class TrieNode:
def __init__(self):
self.children = {} # Character -> TrieNode
self.is_word = FalseEach node also needs to know whether it marks the end of a word, which we track with a boolean flag. For example, in a trie containing both do and done, the node for o needs to flag itself as the end of a word, while still having a child node for n. Tries make it easy to check if a string is a complete word or just part of a longer one.
Inserting into a trie is like creating a new path through a forest, where we add new routes only when we need them. Starting at the root node, we walk through each character of our word, either following existing paths or creating new ones. If we want to insert cow and the trie already has cook, we reuse the path for co and only create a new node for w. That sharing is what makes tries memory-efficient.
# Insert word into trie - O(m)
def insert(self, word: str) -> None:
curr = self.root
# Create path for each character
for char in word:
if char not in curr.children:
curr.children[char] = TrieNode()
curr = curr.children[char]
# Mark the end of the word
curr.is_word = True
Once the path for the word exists, we need to mark its end. Setting an end-of-word flag on the final node lets us tell complete words apart from prefixes. The isWord boolean keeps us from treating partial matches as valid words during search, and lets us store multiple words that share the same beginning while still knowing exactly where each one ends.
Deleting from a trie takes more care than insertion because we have to preserve shared paths used by other words. The first step is simple. We follow the path of characters and unmark the end-of-word flag when we land on our target word. For example, if the trie has both to and top and we want to delete to, we just remove its word marker. The nodes themselves stay in place because top still needs them.
# Delete word from trie - O(m)
def delete(self, word: str) -> None:
curr = self.root
# Follow the path for each character
for char in word:
if char not in curr.children:
return # Word not in trie
curr = curr.children[char]
# Unmark the end of the word
curr.is_word = FalseThe deletion process follows the word's path through the trie, checking each character one by one. If at any point we can't find the next character, or if we reach the end and it isn't marked as a word, there's nothing to delete and we return. When we do find the word, we set its end-of-word flag to False and leave the nodes in place for any other words that share the path.
Searching in a trie is like following a trail of breadcrumbs. We check each character one at a time, starting from the root, and follow child nodes that match. If we ever can't find a matching child, the word isn't in the trie. Even if we make it to the end of the word, we still have to check whether that final node is marked as a word. Otherwise we'd treat partial matches like "ca" as words just because they're prefixes of something we stored.
# Search for complete word in trie - O(m)
def search(self, word: str) -> bool:
curr = self.root
for char in word:
if char not in curr.children:
return False
curr = curr.children[char]
# Check if this node marks end of a word
return curr.is_word
# Search for prefix in trie - O(m)
def starts_with(self, prefix: str) -> bool:
curr = self.root
for char in prefix:
if char not in curr.children:
return False
curr = curr.children[char]
# Don't need to check is_word for prefix
return True
Searching for prefixes works almost the same way, with one difference: we don't need to check the word flag at the end. If we can follow the path of characters to reach a valid node, the prefix is in the trie. It doesn't matter whether that node marks the end of a word or not. This is exactly what makes tries great for autocomplete, where you need every word that starts with a given prefix.
Tries are the right tool for storing and searching words or sequences when prefixes matter. Autocomplete, spell-check, and word puzzles are classic examples. Here are a few tips for using tries well in coding interviews:
A method where a function calls itself to solve smaller instances of the same problem.
An algorithmic technique for solving problems by exploring all possibilities.
An algorithm for traversing graphs by exploring as far as possible along each branch.
An algorithm for traversing graphs level by level.
Tries make prefix problems collapse from O(n*m) to O(m). Most of the work is just implementing the structure cleanly; the rest is recognizing when a prefix tree is the right tool. Work through these to internalize the patterns:
Completed | Title | Status | Notes | Solution | |
|---|---|---|---|---|---|
Easy | |||||
Easy | |||||
Medium | |||||
Medium | |||||
Medium | |||||
Medium | |||||
Medium | |||||
Medium | |||||
Hard | |||||
Hard | |||||
Hard | |||||
Hard |